# AGENTS.md — DBFlux Guidelines for AI agents working in this Rust/GPUI codebase. ## Project Overview DBFlux is a keyboard-first database client built with Rust and GPUI (Zed's UI framework). For project structure, crate boundaries, key files, and subsystem overviews, use `ARCHITECTURE.md` as the canonical reference. For the branching model, version rules, tag flow, and release procedure, use `docs/RELEASE.md` as the canonical reference. For contributor workflow and the label taxonomy, use `CONTRIBUTING.md`. The automated release skill (`skills/dbflux-release/SKILL.md`) follows the same rules. ## Issues and Pull Requests Both are created from the repository's own templates — never from a remembered or invented shape: - An issue uses one of the forms in `.github/ISSUE_TEMPLATE/` — `bug_report.yml` for a defect, `feature_request.yml` for a capability. Fill the form's fields in the order it declares them, and keep the `[bug] ` / `[feature] ` title prefix and the `Labels I plan to apply` checklist it renders. - A pull request uses `.github/pull_request_template.md` verbatim: `Summary`, `What does this resolve?`, `How was this solved?`, `Validation`, `Where was this tested?`, `Checklist`, `Labels to apply`. Do not add sections of your own; follow-up work and known limitations belong where the template asks for them. - The mechanics (duplicate search, choosing the form, `gh` commands, reading the full branch delta) live in `skills/issue-creation/SKILL.md` and `skills/branch-pr/SKILL.md`. - Use only labels from the taxonomy in `CONTRIBUTING.md` § Label Guide, and never invent labels such as `area:*`. - A contributor without write access to `0xErwin1/dbflux` cannot apply labels from a fork. State the labels the change needs where the template asks for them and ask a maintainer to apply them during triage — do not drop that part of the template. ## Build & Run Commands ```bash cargo check --workspace # Fast type checking cargo build -p dbflux --features sqlite,postgres,mysql,mssql,mongodb,redis,dynamodb,cloudwatch,influxdb,redshift,s3,aws # Debug build cargo build -p dbflux --features sqlite,postgres,mysql,mssql,mongodb,redis,dynamodb,cloudwatch,influxdb,redshift,s3,aws --release # Release build cargo run -p dbflux --features sqlite,postgres,mysql,mssql,mongodb,redis,dynamodb,cloudwatch,influxdb,redshift,s3,aws # Run app # MCP server (AI integration) - included by default cargo build -p dbflux # MCP included in default features ./target/debug/dbflux mcp --client-id test-client # Build without MCP support (smaller binary, no AI integration) cargo build -p dbflux --no-default-features --features sqlite,postgres,mysql,mssql,mongodb,redis,dynamodb,cloudwatch,influxdb,redshift,s3,lua,aws python3 scripts/lint.py fmt # Format python3 scripts/lint.py clippy # Lint cargo test --workspace # All tests cargo test --workspace test_name # Single test cargo test -p dbflux_core # Tests in specific crate cargo test -p dbflux_driver_dynamodb --test live_integration -- --ignored # Docker-backed live tests # Preferred test runner: always use `cargo nextest run` over `cargo test` when # available (provided by the Nix dev shell). It is faster and gives clearer # output. Note it does NOT run doctests, so run those separately. cargo nextest run --workspace # All tests (unit + integration) cargo test --doc --workspace # Doctests (run separately) cargo nextest run -p dbflux_driver_sqlite --run-ignored all # Include #[ignore]d live tests # Nix nix develop # Enter dev shell nix build # Build package nix run # Run directly ``` `scripts/lint.py` covers only the first-party crates under `crates/` and never lints or reformats the vendored crates under `vendor/`. **Linux build requirement**: `.cargo/config.toml` links the `x86_64-unknown-linux-gnu` target with `-fuse-ld=mold`, so the `mold` linker must be on `PATH` for any local `cargo build`/`test`/`check`. The Nix dev shell and CI provide it; non-Nix Linux setups must install `mold` via their package manager. Windows and macOS use their default linker and are unaffected. The renderer's font stack needs no fontconfig development headers: `.cargo/config.toml` sets `RUST_FONTCONFIG_DLOPEN`, so `libfontconfig` is loaded at runtime instead of linked, and a machine without it loses font discovery rather than failing to start the binary. freetype follows its own build script, which uses the system library when a `freetype2.pc` of at least the required version is present and otherwise compiles its vendored sources, so a missing development package costs build time rather than breaking the build. ## Verifying UI Changes with UI Automation A build with the `ui-automation` feature exposes every DBFlux window to a local MCP server, so an agent can check a UI change in the running app: read the element tree, click, type and take screenshots. Use it after a change to rendering, focus, keyboard handling or a user flow, in addition to the GPUI tests, not instead of them. Setup, the full tool list and the trust model are in `docs/UI_AUTOMATION.md`. **Setup (once per machine).** Build the server and register it with your MCP client. Claude Code only loads a newly registered server in a new session, so if the `dbflux-ui` tools are not available, ask the user to register it and restart rather than working around it: ```bash cargo build -p gpui-mcp-server claude mcp add dbflux-ui -- "$PWD/target/debug/gpui-mcp" --app-id dbflux ``` **Launching DBFlux for a session.** Always isolate the data directories, so the session never reads or writes the user's real configuration, connections or `dbflux.db`. On a Wayland session, clear `WAYLAND_DISPLAY` so DBFlux runs under XWayland, which screenshots need. Run it in the background and record its PID: ```bash scratch="$(mktemp -d)" WAYLAND_DISPLAY= GPUI_X11_SCALE_FACTOR=1 \ XDG_DATA_HOME="$scratch/data" XDG_CONFIG_HOME="$scratch/config" \ XDG_STATE_HOME="$scratch/state" XDG_CACHE_HOME="$scratch/cache" \ cargo run -p dbflux --features ui-automation ``` **Working loop.** 1. Find the target with `get_ui_tree` or `find_elements`. Elements are addressed by their GPUI element id (`.id("rail-scripts")` becomes `rail-scripts`). 2. Act with `click_element`, `type_text`, `set_text` or `keyboard` (GPUI keystroke syntax, for example `ctrl-shift-p`). 3. Wait for the result with `wait_for_element` or `wait_for_state` instead of sleeping. 4. Check the outcome in the element tree, and with `screenshot` when the change is visual. Look at the image before reporting that the change works. **Rules.** - Give new interactive elements (buttons, tabs, inputs, list rows) a stable, descriptive `.id(...)`, so they can be targeted by name rather than by coordinates. - Mark new secret inputs with `controls::Input::secret(true)`, or derive the flag from `form_renderer::is_secret_field`, so their value stays out of the element tree. - Connect only to local test databases (SQLite files in the scratch directory, or the Docker services used by the live integration tests). A query run through the UI bypasses MCP policies and approvals. - Stop the DBFlux process when the session ends, and never commit screenshots or recordings. - Never add `ui-automation` to a `default` feature list. ## Rust Guidelines ### General Principles - Prioritize correctness and clarity over speed - Do not write comments that summarize code; only explain non-obvious "why" - Prefer implementing in existing files unless it's a new logical component - Avoid creating many small files - Avoid creative additions unless explicitly requested - Use full words for variable names (no abbreviations like "q" for "queue") ### Error Handling - Avoid `unwrap()` and functions that panic; use `?` to propagate errors - Be careful with indexing operations that may panic on out-of-bounds - **Never use `let _ =` on a fallible expression.** Silently discarding `Result` / `Option` errors hides real failures from users, logs, and the audit trail. This rule has no exceptions for "fire and forget" — pick one of the alternatives below instead: - **Propagate** with `?` when the caller should handle it. - **Log** with `.log_err()` (from `dbflux_core`) when ignoring but wanting at least one stderr/audit trace. - **Branch explicitly** with `match` / `if let Err(e) = ...` when you need custom logic. - **Surface to the user** with `report_error` / `report_error_async` (`dbflux_ui_base::user_error`) when the failure is something the user just triggered. Producing a toast + audit row is preferred over `log::error!` for any user-facing operation; this also drives the status-bar error badge. - If you genuinely want to drop a value — not an error — bind it to `_name` instead of `_`. Reserve the bare `let _ =` pattern for that intent only. - Ensure async errors propagate to the UI so users get meaningful feedback. Background tasks that fail without a path to the foreground are a bug, not a feature. #### User-facing error reporting (`dbflux_ui_base::user_error`) When a user-triggered operation fails (storage, network, driver, config, hook, auth), route it through the centralized seam instead of `log::error!` + manual `Toast::error(...)`. The seam attaches a UUID v7 correlation id to the toast and the audit row, drives the status-bar error badge, and provides a "View in Audit" action wired to that correlation id. - Foreground (`&mut App` / `&mut Context`): `report_error(UserFacingError::new(ErrorKind::Storage, msg), cx)` - Background (`cx.spawn(async ...)` / `background_executor`): `report_error_async(UserFacingError::new(ErrorKind::Network, msg), &cx)` - Driver errors: prefer `UserFacingError::from_formatted(ErrorKind::Driver, fe)` so the driver's `ErrorFormatter` output feeds `cause` directly — keeps the UI driver-agnostic. - `ErrorKind` variants: `Storage`, `Network`, `Auth`, `Hook`, `Driver`, `User`, `Config`. - Convention: only the **first catch site** reports. Propagators above must NOT re-report — there is no runtime deduplication, double-toasts are a code-review concern. ### File Organization - Use `mod.rs` for module directories (e.g., `views/mod.rs`, not a sibling `views.rs`) - When creating crates, specify library root in `Cargo.toml` with `[lib] path = "..."` ### Async Patterns Use variable shadowing to scope clones in async contexts: ```rust executor.spawn({ let task_ran = task_ran.clone(); async move { *task_ran.borrow_mut() = true; } }); ``` ### Performance Patterns **Pre-compute expensive operations**: Move string formatting and allocation into constructors rather than during rendering: ```rust // Good: Format once during construction CellValue::Text { display: format!("{}", value), ... } // Bad: Format on every render fn render(&self) { format!("{}", self.value) } ``` **Lazy loading for large datasets**: Drivers should return shallow metadata initially and fetch details on-demand: ```rust fn get_tables(&self) -> Vec // Names only fn table_details(&self, name: &str) -> TableDetails // Columns, indexes ``` **Driver error formatting**: Drivers implement the `ErrorFormatter` trait from `dbflux_core/src/core/error_formatter.rs` to extract detailed error info. PostgreSQL's `as_db_error()` provides detail, hint, column, table, and constraint fields. MongoDB extracts error codes and labels. Use structured error formatting instead of raw `format!("{:?}", e)`. ## GPUI Guidelines ### Context Types - `App` — root context for global state and entity access - `Context` — provided when updating `Entity`, derefs to `App` - `AsyncApp` / `AsyncWindowContext` — from `cx.spawn`, can cross await points - `Window` — window state, passed before `cx` when present ### Entity Operations With `thing: Entity`: - `thing.read(cx)` → `&T` - `thing.update(cx, |thing, cx| ...)` → mutate with `Context` - `thing.update_in(cx, |thing, window, cx| ...)` → also provides `Window` Use the inner `cx` inside closures, not the outer one, to avoid multiple borrows. ### Concurrency All entity/UI work happens on the foreground thread. ```rust // Background work + foreground update let task = cx.background_executor().spawn(async move { // expensive work }); cx.spawn(async move |_this, cx| { let result = task.await; cx.update(|cx| { entity.update(cx, |state, cx| { state.pending_result = Some(result); cx.notify(); }); }).ok(); }).detach(); ``` Task handling: - Await in another async context - `task.detach()` or `task.detach_and_log_err(cx)` for fire-and-forget - Store in a field if work should cancel when struct drops ### Rendering Types implement `Render` for element trees with flexbox layout: ```rust impl Render for MyComponent { fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { div().border_1().child("Hello") } } ``` - Use `.when(condition, |this| ...)` for conditional attributes/children - Use `.when_some(option, |this, value| ...)` for Option-based conditionals - Call `cx.notify()` when state changes affect rendering ### Font Size Scaling - Interface sizes that follow the UI font size are rems built with `dbflux_components::tokens::ui(px)`: font sizes, text row and control heights, inline icons, and widths that hold text. Spacing, borders, radii and layout panel sizes stay `Pixels`. - Every window must render under `gpui_component::Root`, which sets the rem size from the UI font size. A window without it keeps the default 16 px rem and ignores the setting. - Pixel arithmetic on a rem token converts first: `token.to_pixels(window.rem_size())`, or `fonts::ui_px(cx, token)` without a `Window`. - The data grid and the code editor follow their own font sizes: use `fonts::grid_*` / `fonts::grid_px` and `fonts::editor_*` / `fonts::editor_scaled`, not `tokens::ui`. - Do not write `text_size(px(..))`. The `dbflux_components` style guardrails reject it in every UI crate. ### Entity Updates in Render Use `pending_*` fields with `.take()` to safely update other entities or open modals: ```rust fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { if let Some(data) = self.pending_data.take() { self.other_entity.update(cx, |other, cx| { other.apply(data, window, cx); }); } // For modals: defer open until render if let Some(modal) = self.pending_modal_open.take() { self.modal.update(cx, |m, cx| m.open(modal.value, window, cx)); } // render UI... } ``` ### Input & Actions Event handlers: `.on_click(cx.listener(|this, event, window, cx| ...))` Actions defined with `actions!(namespace, [SomeAction])` macro or `#[derive(Action)]`. ### Keyboard & Mouse Patterns **Activation vs press**: Use `.on_click(...)` for activations (open, toggle, select, run); it fires on release and gives the element an accessibility click action. Use `.on_mouse_down(MouseButton::Left, ...)` only for press semantics (drag start, resize grip, focus guard, backdrop dismiss, keyboard cursor sync), list each such handler in `scripts/mouse_down_allowlist.txt`, and check with `python3 scripts/lint.py mouse-down`. **Keyboard coverage**: Every clickable element needs a keyboard path (a bound `Command` or a menu entry) registered in its surface's coverage registry. The coverage tests fail on an unregistered id. See `CONTRIBUTING.md` § Keyboard Coverage. **Focus tracking**: Use `.track_focus(&focus_handle)` on container elements to receive key events: ```rust div() .track_focus(&self.focus_handle) .on_key_down(cx.listener(|this, event, window, cx| { ... })) .child(content) ``` **Mouse/keyboard sync**: When a component supports both mouse and keyboard navigation, sync state on mouse events: ```rust .on_mouse_down(MouseButton::Left, cx.listener(|this, _, _, cx| { this.focus_mode = FocusMode::SomeMode; this.edit_state = EditState::Editing; cx.notify(); })) ``` **Input blur race condition**: When switching between inputs via click, the old input's `Blur` event fires after the new input's `mousedown`. Use a flag to prevent focus theft: ```rust // In mousedown handler this.switching_input = true; // In blur handler / exit_edit_mode if self.switching_input { self.switching_input = false; return; } ``` **Focus state machines**: For complex focus scenarios (e.g., toolbar with editable inputs), use explicit state enums: ```rust enum FocusMode { Table, Toolbar } enum EditState { Navigating, Editing } ``` ### Subscriptions ```rust cx.subscribe(other_entity, |this, other_entity, event, cx| ...) ``` Returns `Subscription`; store in `_subscriptions: Vec` field. ### Deprecated Types (NEVER use) - `Model`, `View` → use `Entity` - `AppContext` → use `App` - `ModelContext` → use `Context` - `WindowContext`, `ViewContext` → use `Window` + `Context` ## Architecture Rules Architecture details live in `ARCHITECTURE.md`. This file only keeps the agent-facing rules that affect how changes should be made. ### UI crate split (6 crates) The UI layer is split into six crates (see `ARCHITECTURE.md` § Layered crate map for the full diagram): - `dbflux_components` — domain-free leaf: theme, tokens, icons, primitives, composites, controls, data_table, document_tree, result_panel, chart engine, modals. No `dbflux_app` dependency. May depend on `dbflux_i18n` for translated UI copy; `dbflux_i18n` has no `dbflux_*` dependencies. - `dbflux_ui_base` — AppStateEntity, events, keymap helpers, toast, platform detection, sql_preview_modal, sso_wizard. - `dbflux_ui_document` — tab/pane system, all document types (CodeDocument, DataDocument, ChartDocument, DashboardDocument, KeyValueDocument, AuditDocument, InstanceInspectorDocument, BucketsTableDocument, ObjectBrowserDocument, ObjectEditorDocument), data_grid_panel, governance view. - `dbflux_ui_sidebar` — connections + scripts sidebar tree. - `dbflux_ui_windows` — settings window and connection manager window. - `dbflux_ui` — thin integrator (~16k LOC): workspace, status_bar, tasks_panel, dock, remaining overlays (command_palette, login_modal, shutdown_overlay), keymap glue, assets, ipc_server, app_menu (the macOS application menu bar). Re-exports moved subsystems via `pub use` shims at the old module paths so internal call-sites still compile against `crate::ui::...`. `dbflux_ui` has **no per-driver feature flags** and no driver dependencies. Per-driver features live on `dbflux_app` (which registers drivers) and on the `dbflux` binary. The cross-cutting `lua`/`aws`/`mcp` features on UI crates only forward to `dbflux_app` and sibling UI crates. ### Driver/UI Decoupling **Never add driver-specific logic in UI code.** The UI must remain agnostic to specific database implementations. **This rule is strict and applies to both `dbflux_ui` and app-layer orchestration code.** Do not branch on concrete driver IDs or driver names in the app/UI layer, and do not add direct references to specific drivers there unless the code is only registering/building the driver itself. In practice, this means: - No `if driver_id == "..."` or `match driver_id` checks in `dbflux_ui` or app-facing workflow code. - No CloudWatch/MongoDB/Redis/etc. special cases in document rendering, sidebar routing, workspace tab opening, or query-context controls. - The core must expose the seam the UI needs, and the driver must populate or implement that seam. - The UI may only respond to generic core abstractions such as metadata, capabilities, collection presentation hints, child-source descriptors, event-stream targets, and source-context specs. Instead of: ```rust // BAD: Driver-specific conditional in UI if driver_id == "mongodb" { show_document_view(); } else { show_table_view(); } ``` Use abstractions from `DriverMetadata`: ```rust // GOOD: Use capability flags and metadata match metadata.category { DatabaseCategory::Document => show_document_view(), DatabaseCategory::Relational => show_table_view(), _ => show_generic_view(), } // GOOD: Use query language for editor behavior let placeholder = metadata.query_language.placeholder(); let editor_mode = metadata.query_language.editor_mode(); ``` Key abstractions for UI adaptation: - `DatabaseCategory`: Determines view mode (table vs document tree), terminology (rows vs documents) - `QueryLanguage`: Determines editor syntax highlighting, placeholder text, comment prefix - `DriverCapabilities`: Determines which features to enable (pagination, transactions, etc.) - `CollectionPresentation`: Determines how a collection/container opens (for example data grid vs event stream) - `CollectionChildInfo`: Declares driver-owned child sources that appear in the sidebar without the UI inferring them from driver-specific conventions - `EventStreamTarget`: Lets the workspace/audit viewer open driver-backed event streams without embedding driver-specific routing - `SourceContextSpec`: Lets drivers declare extra query-context controls while the UI stays generic - `FormFieldKind::AuthProfileRef { provider_id: Option }`: Drives the connection-manager auth-profile dropdown. Drivers that need the picker declare a `profile` field as `AuthProfileRef { provider_id: None }`; the UI renders and field-syncs this arm generically instead of matching driver ids (DEC-1). A `None` filter enumerates built-in and external RPC-backed providers alike. ### Generic Deduplication Patterns **`JsonStore`**: Single generic JSON-file store with type aliases (`ProfileStore`, `SshTunnelStore`, `ProxyStore`). Named constructors (`.profiles()`, `.ssh_tunnels()`, `.proxies()`) set the filename. **`ItemManager`**: CRUD manager with auto-save, backed by `JsonStore`. Uses `Identifiable` trait for ID access and `DefaultFilename` trait for `Default` on type aliases. `ProxyManager` and `SshTunnelManager` are type aliases. `ProfileManager` stays separate (has extra methods like `find_by_id`, `profile_ids`). **`HasSecretRef`**: Unifies secret operations for types with keyring references (`SshTunnelProfile`, `ProxyProfile`, `AuthProfile`). `SecretManager` generic methods (`get_secret`, `save_secret`, `delete_secret`) delegate through this trait. **`FormGridNav`**: 2D grid navigation for settings forms. Takes `&[Vec]` rows as input to each method (not stored), so callers compute dynamic grids from their own state. Used by proxy and SSH tunnel settings forms. **`TreeNav`**: Reusable tree navigation component (plain struct, not a GPUI Entity). Supports cursor movement, expand/collapse, select-by-id. Used by Settings sidebar and connections sidebar. ### RPC Services Foundation - RPC services are first-class persisted descriptors with `RpcServiceKind` (`Driver`, `AuthProvider`). - Both `Driver` and `AuthProvider` services are active. `AuthProvider` services connect through `RpcAuthProvider` in `dbflux_ipc`, which implements `DynAuthProvider` from `dbflux_core`. - The runtime seam for service discovery/classification lives in `dbflux_app::rpc_services`; extend that boundary for future RPC capabilities instead of hardcoding new driver-only bootstrap logic in `app_state.rs`. - Preserve compatibility for external driver registration IDs as `rpc:`. - `DynAuthProvider::fetch_dynamic_options` is a real trait method (default implementation returns `Permanent("not supported")`); `RpcAuthProvider` implements it by dispatching `FetchDynamicOptions` requests over IPC. - The auth-provider IPC protocol is at v1.3. v1.2 added the `FetchDynamicOptions` request / `DynamicOptions` response variants plus the `secret_dependency_opt_in` manifest flag; v1.3 added the `audit_emit_opt_in` manifest flag and audit event emission (`EmitAuditEvent`). Providers advertising v1.2+ have `fetch_dynamic_options` available; older providers get `Permanent("not supported")`. - The Settings UI for Auth Profiles is provider-agnostic. To surface dynamic dropdowns a provider must declare `FormFieldKind::DynamicSelect` fields in its manifest. The host strips secret field values from dependency maps unless the provider sets `secret_dependency_opt_in: true`. - `AuthSession.data` round-trips opaquely through the IPC DTO via JSON downcast and is never persisted. ### Connection Hooks - Hooks are reusable command definitions (name, command, args, cwd, env, timeout, failure policy) - Hook execution modes are `Command`, `Script`, and `Lua` - Process-backed hooks can be inline or file-backed; Lua hooks run in-process through `dbflux_lua` - Profile phase bindings: PreConnect, PostConnect, PreDisconnect, PostDisconnect - `HookRunner` orchestrates execution with `HookPhaseOutcome` (success/warning/abort) - Each hook runs as its own background task with stdout/stderr visible in Tasks panel - Process-backed hooks and `dbflux.process.run()` share the same streaming executor in `dbflux_core`; avoid duplicating process execution logic - Editor-run Lua scripts use `LuaCapabilities::all_enabled()` and stream live output into a document-owned buffer via channel, not a shared mutex string - Failure policies: Disconnect (abort flow), Warn (continue with warning), Ignore (log only) - Hooks section in Settings for global definitions; Hooks tab in Connection Manager for per-profile bindings - Types and logic in `dbflux_core/src/connection/hook.rs`, UI in `crates/dbflux_ui_windows/src/settings/hooks.rs` and `crates/dbflux_ui_windows/src/connection_manager/hooks_tab.rs` ### Adding a New Driver 1. Create `crates/dbflux_driver_/` 2. Implement `DbDriver` and `Connection` from `dbflux_core` 3. Define `DriverMetadata` with appropriate `DatabaseCategory`, `QueryLanguage`, and `DriverCapabilities` 4. Define the connection form in the driver crate (e.g. `const DRIVER_FORM: DriverFormDef`) and return it from `DbDriver::form_definition()`. Form definitions live with the driver, not in `dbflux_core`. 5. Implement `ErrorFormatter` for driver-specific error messages 6. Implement `QueryGenerator` when the driver can generate native mutation/read templates for UI previews, copy-as-query, or MCP previews 7. Implement `LanguageService` when the driver speaks a non-SQL dialect (e.g. `TSqlLanguageService` lives in `dbflux_driver_mssql`). SQL drivers can reuse `SqlLanguageService` from `dbflux_core`. 8. Add feature flag in `crates/dbflux/Cargo.toml` (binary) and `crates/dbflux_app/Cargo.toml`. No UI crate gains a per-driver feature flag. The feature MUST also be added to `default` in `crates/dbflux/Cargo.toml` — release builds ship default features, so a driver left out of `default` silently ships disabled. 9. Register in `AppState::new()` under `#[cfg(feature = "name")]` 10. Register the driver's `live_integration` test suite as its own step in `.github/workflows/tests.yml`'s Driver Live Integration job — it enumerates suites explicitly, so a new driver's Docker-backed tests do not run in CI until added there. 11. **Set `ColumnMeta::kind` on every column** using the `ColumnKind` enum (Timestamp, Float, Integer, Text, Unknown). The chart engine uses `ColumnKind` exclusively — it never inspects `type_name` strings or driver identifiers. Columns with `kind = Unknown` are excluded from chart auto-detection. Use `ColumnKind::Timestamp` for time columns, `ColumnKind::Float`/`Integer` for numeric columns, and `ColumnKind::Text` for string columns. 12. Optional: implement `DashboardSource` and/or `DashboardImporter` and advertise `DriverCapabilities::DASHBOARD_SYNC` / `DASHBOARD_IMPORT` to let the UI browse/import upstream dashboards (see `docs/DASHBOARDS.md`). 13. Optional: implement `InstanceCatalog` (`dbflux_core/src/connection/instance_catalog.rs`) and advertise `DriverCapabilities::INSTANCE_METRICS` (time-series) and/or `INSTANCE_INSPECTOR` (tabular snapshots). The catalog exposes metrics, inspectors, a `DefaultInstanceDashboard` descriptor for the read-only Instance Overview, and optional `InspectorRowAction`s gated by per-driver privilege probes. See `docs/DASHBOARDS.md` § Instance Overview and inspectors. For external RPC-backed drivers, keep discovery/adaptation in `dbflux_app::rpc_services` rather than adding a parallel bootstrap path. ### Driver Capabilities Drivers declare their capabilities via `DriverMetadata`: - `DatabaseCategory`: Relational, Document, KeyValue, Graph, TimeSeries, WideColumn - `QueryLanguage`: SQL, MongoQuery, RedisCommands, Cypher, etc. (determines editor syntax highlighting and placeholder) - `DriverCapabilities`: bitflags for features (PAGINATION, TRANSACTIONS, NESTED_DOCUMENTS, etc.) ### Documentation translations - `docs/es/` and `docs/zh_Hans/` mirror `docs/`, the driver READMEs, and the root documents the site renders (`ARCHITECTURE.md`, `CONTRIBUTING.md`, `SECURITY.md`, `TRADEMARK.md`, `PRIVACY.md`). - Any edit to one of those English pages must be applied to every counterpart that already exists under `docs/es/` and `docs/zh_Hans/`, in the same change. Do not leave a translation for a follow-up. - A page with no counterpart yet needs none; the site serves the English body with a notice. - Keep headings, relative links, and ```mermaid fences aligned with the English page, because the site pairs pages by structure. See `docs/TRANSLATIONS.md`. ### Driver README documentation - Every driver crate under `crates/dbflux_driver_*/` must include a `README.md`. - Keep each driver README focused on two sections: **Features** and **Limitations**. - Update driver README files whenever capabilities, supported operations, or known limits change. ### Document System Pattern Documents are open-tab entities managed through a closure-erasing shell. The polymorphism mechanism is `PaneHandle`, not a closed enum. See `ARCHITECTURE.md` § Document System for the full picture. 1. **Shell**: `PaneHandle` (`crates/dbflux_ui_document/src/pane.rs`) wraps the typed `Entity` with `Box` closures for 22 operations (render, focus, dispatch_command, meta_snapshot, dedup, subscribe, etc.). `PaneHandle` is `!Clone`. Each document provides `XxxDocument::into_pane(entity, cx) -> PaneHandle` in its own `pane.rs`. 2. **Tab**: `Tab::Pane(Box)` (`crates/dbflux_ui_document/src/tab_manager.rs`) — `#[non_exhaustive]` single-variant enum for forward-compat. 3. **Event**: documents emit `DocumentEvent` directly (`crates/dbflux_ui_document/src/handle.rs`, 29 LOC). No per-document event enums. 4. **Dedup**: `DocumentKey` enum (`crates/dbflux_ui_document/src/dedup.rs`) — variants `Table`, `Collection`, `File`, `KeyValueDb`, `Chart`, `Audit`, `EventStream`, `Routine`, `MetricChart`, `Dashboard`, `InstanceMetric`, `InstanceInspector`, `InstanceOverview`, `ObjectStoreBucketsRoot`, `ObjectBrowser`, `ObjectEditor`. Find existing tabs via `tab_manager.find_by_key(&DocumentKey::Table { ... }, cx)`. No `is_*` methods. 5. **Chrome**: `ResultPanel` + `ViewHandle` (`dbflux_components::result_panel`) is the universal chrome host for data-result views. View entities expose `into_view_handle(entity, cx) -> ViewHandle` whose `toolbar_segments` closure returns `ToolbarSegment`s positioned `Left | Center | Right` with `index`. Filter bars, axis bars, range chips all become segments — the chrome row uses `flex_wrap` so segments wrap when narrow. 6. **Scripts**: Lua/Python/Bash use `CodeDocument` and execute as scripts, not DB queries; script output streams into `crates/dbflux_ui_document/src/code/live_output.rs`. 7. **Focus**: Documents receive `FocusTarget::Document` and manage internal focus via their own `FocusHandle`. **Adding a new document type** (zero changes to `workspace/mod.rs`, `tab_manager.rs`, `tab_bar.rs`, `handle.rs`): 1. Create `crates/dbflux_ui_document/src//mod.rs` with the entity 2. Create `crates/dbflux_ui_document/src//pane.rs` with `into_pane(entity, cx) -> PaneHandle` 3. Add a `DocumentKey` variant in `crates/dbflux_ui_document/src/dedup.rs` if dedup is needed 4. Add `open_` in `crates/dbflux_ui/src/ui/views/workspace/actions.rs` **Known constraint**: `KeyValueView` and `LogStreamView` are boundary structs in their `view.rs` files, NOT separate GPUI entities. `impl Render` remains on the host document because GPUI's single-`Context` borrow model with `cx.listener()` closures over `Self` makes entity-level extraction infeasible without relocating all domain state. The boundary is file-level (render helpers + render code in sibling files), not entity-level. ### MCP Governance System DBFlux supports the Model Context Protocol (MCP) for AI client integration with a complete governance layer: **Classification**: Operations are classified by impact level via `ExecutionClassification`: - `Metadata` — Schema introspection (list tables, describe object) - `Read` — SELECT queries, data browsing, read-only previews - `Write` — INSERT/UPDATE, mutations - `Destructive` — DELETE, DROP, TRUNCATE - `AdminSafe` — Safe DDL operations (CREATE TABLE, CREATE INDEX, ADD COLUMN with default/nullable) - `Admin` — Risky DDL operations (DROP COLUMN, RENAME COLUMN, ALTER COLUMN, DROP INDEX) and privileged admin flows - `AdminDestructive` — Irreversible DDL operations (DROP TABLE, DROP DATABASE, TRUNCATE TABLE) **Policy Engine** (`dbflux_policy`): - `PolicyEngine::evaluate()` takes actor, connection, tool, and classification - Returns `PolicyDecision::Allow` or `PolicyDecision::Deny(reason)` - Supports roles with policy composition and connection-scoped assignments - `TrustedClientRegistry` identifies known AI clients **Approval Flow** (`dbflux_approval`): - Destructive or write operations can require human approval - `InMemoryPendingExecutionStore` holds deferred executions - `ApprovalService` manages approve/reject lifecycle **Audit** (`dbflux_audit`): - SQLite-backed audit log in `~/.local/share/dbflux/dbflux.db` (`aud_audit_events` table) - Events use the `EventRecord` type from `dbflux_core::observability` with category, severity, outcome, actor type, and structured fields - Events are emitted through the `EventSink` trait — inject `Arc` into service layers rather than calling `AuditService` directly - Categories: `Query`, `Connection`, `Hook`, `Script`, `Mcp`, `Governance`, `Config`, `System` - By default: sensitive values are redacted, query text is replaced with a SHA256 fingerprint (not stored in full), details_json is capped at 64 KiB - Queryable via `AuditQueryFilter` (actor, tool, category, action, outcome, date range, free text) - Export to JSON/CSV via `AuditService::export()` (basic) or `export_extended()` (all fields including details_json) - Purge old events by retention policy: `AuditService::purge_old_events(days, batch_size)` - See `docs/AUDIT.md` for the full event schema, required fields per category, and usage patterns **Runtime** (`dbflux_mcp`): - `McpRuntime` implements `McpGovernanceService` trait - Integrates policy engine, approval service, and audit service - Emits `McpRuntimeEvent` for UI updates - Tool catalog defines canonical MCP tools and deferred tools **Important runtime rules**: - `preview_mutation` must stay read-only; it may return generated SQL/query text or a non-mutating plan, but must never execute the mutation being previewed - `preview_ddl` is intentionally not exposed from the MCP surface until DBFlux has a truly safe schema-preview path - `select_data` runs `joins` only on a connection whose driver declares `QueryCapabilities::supports_joins` and whose `QueryGenerator::generate_select` renders the spec; every other connection gets an explicit error, never a result with the join left out. The `on` condition is parsed into structured column comparisons (`parse_join_condition` in `dbflux_core`), table, alias and column names must be plain identifiers, and the generated query must classify as a read before it runs. Never build `JoinOn::RawExpression` from tool input **Trust model**: MCP authentication is process-identity only: presenting `--client-id` is the sole authentication signal. Any local process that knows the client ID can connect. This is not a cryptographic guarantee. Do not expose the MCP server beyond localhost without an additional authentication layer. A cryptographic MCP auth layer is a known follow-up item, not yet scheduled. **Standalone Server** (`dbflux_mcp_server`): - Integrated as subcommand: `dbflux mcp --client-id ` for AI clients - Communicates via JSON-RPC over stdin/stdout - Uses same governance stack as in-app MCP - Optional: Can be disabled with `--no-default-features` at build time **UI Integration**: - `McpApprovalsView` document for reviewing pending executions - MCP settings section for trusted clients, roles, and policies - `AuditDocument` as the unified audit viewer for all event categories (no separate MCP audit surface) - `LoginModal` and `SsoWizard` overlays for AWS SSO authentication flow ### Dashboards & Saved Charts - Saved charts and dashboards are persisted in SQLite under the `viz_*` table prefix (`viz_dashboards`, `viz_dashboard_panels`, `viz_saved_charts`, `viz_saved_chart_series`, `viz_saved_chart_binding_y`, `viz_saved_chart_source_metric_*`). Repositories live in `crates/dbflux_storage/src/repositories/viz_*.rs`. - In-memory managers wrap the repositories: `DashboardManager` (`crates/dbflux_ui_base/src/dashboard_manager.rs`) for `Dashboard` / `DashboardPanel` / `DashboardPanelKind { Chart { saved_chart_id } | Divider { markdown } | Inspector { metric_id } }`, and `SavedChartManager` (`crates/dbflux_ui_base/src/saved_chart_manager.rs`) for `SavedChart` + `SavedChartRefreshPolicy`. Writes go to the repo first; caches update only on success. - `DashboardDocument` (`crates/dbflux_ui_document/src/dashboard/`) hosts a 12-column grid of chart, divider, and inspector panels with a shared `TimeRangePanel`. Dedup keys: `DocumentKey::Dashboard { dashboard_id }` (persisted) or `DocumentKey::InstanceOverview { profile_id }` (auto-generated read-only Instance Overview). - Refresh timers (dashboard, standalone chart, inspector) check `AppState::connections()` before each tick and skip work when the underlying profile is disconnected; the timer itself stays alive so refresh resumes on reconnect without re-arming. - Driver seams (UI never branches on driver id): - `DashboardSource` (`dbflux_core/src/connection/dashboard_source.rs`) — lists upstream dashboards; gated by `DriverCapabilities::DASHBOARD_SYNC`. - `DashboardImporter` (`dbflux_core/src/connection/dashboard_import.rs`) — parses upstream JSON into `WidgetImportSpec`s; gated by `DriverCapabilities::DASHBOARD_IMPORT`. - `InstanceCatalog` (`dbflux_core/src/connection/instance_catalog.rs`) — exposes per-driver metrics, inspectors, default-dashboard descriptor, and row actions; gated by `DriverCapabilities::INSTANCE_METRICS` / `INSTANCE_INSPECTOR`. - CloudWatch is the reference implementation for `DashboardSource` / `DashboardImporter`. PostgreSQL, MySQL/MariaDB, MongoDB, Redis, SQL Server, ClickHouse, and InfluxDB (v2 only) are the reference implementations for `InstanceCatalog`. - Remote dashboard listings are session-scoped via `RemoteDashboardCache` (`crates/dbflux_app/src/remote_dashboard_cache.rs`); they do not persist across restart. Full reference: `docs/DASHBOARDS.md`. ### Visual Query Builder The right-rail builder composes SELECT / UPDATE / DELETE without writing SQL. It is **driver-agnostic**: gated on `QueryLanguage::Sql` with no per-driver branching, so every relational driver gets it. - Specs live in `dbflux_core/src/query/visual_query.rs` (re-exported from `dbflux_core::query`): `VisualQuerySpec`, `VisualMutationSpec`, and `EditableBinding`. - SQL generation extends the existing `QueryGenerator` trait (`dbflux_core/src/query/generator.rs`) with defaulted `generate_select` / `generate_update_from_spec` / `generate_delete_from_spec`; `SqlSelectBuilder` renders the dialect-specific SQL (SQLite, PostgreSQL, MySQL/MariaDB, SQL Server). Add new builder shapes here, not in the UI. - `MutationPolicy` (`dbflux_core/src/connection/manager.rs`) composes to `Allowed` / `ReadOnly` / `ApprovalRequired`. `MutationExecutor` (`crates/dbflux_ui_document/src/data_grid_panel/mutation_executor.rs`) runs the chosen mode: `SingleTransaction`, `ChunkedTransaction` (keyset over the PK), or `DirectAutocommit`. - No-`WHERE` UPDATE/DELETE is gated by the shared dangerous-query dispatcher (see Language Services), not a builder-local check. - UI lives in `crates/dbflux_ui_document/src/query_builder/` (panel, view, `sections/`, `mutation_state`, `completion`, `events`) and integrates into the DataView through `crates/dbflux_ui_document/src/data_grid_panel/`. - Inline edit on builder-generated SELECT results is driven by `EditableBinding`: the result must be *editable-safe* (maps 1:1 to one table with every PK column projected under its original name); otherwise the grid is read-only. The proof lives in `dbflux_core` over generic spec/metadata types — keep it there, not in `dbflux_ui`. - Persistence: migration `017_qry_saved_queries`, the `qry_*` tables, `SavedQueryRepo` (`crates/dbflux_storage/src/repositories/qry_saved_queries.rs`), and the in-memory `SavedQueryManager` (`crates/dbflux_ui_base/src/saved_query_manager.rs`) wired into `AppStateEntity`. Cross-connection import verifies table existence through a `TableProbe` seam rather than reaching into driver code. The document query builder is a separate rail for document collections, with the same rules: - The spec is `DocumentQuerySpec` in `dbflux_core/src/query/document_query.rs`. Drivers implement `DocumentQueryCodec` and return it from `Connection::document_query_codec()`; the codec renders a spec as find slots, a pipeline and preview text, and parses the slots back with the clauses it cannot represent. - The UI gates on `DocumentFeatures::VISUAL_BUILDER` (and `DocumentFeatures::AGGREGATE` for Aggregate mode), never on a driver id. It lives in `crates/dbflux_ui_document/src/document_builder/` and `data_grid_panel/documents/builder.rs`. - Execution goes through the query slots and `browse_collection` (Find) or the Aggregate view (Aggregate); the builder never executes generated shell text. - Persistence: migration `037_qry_saved_document_queries`, `DocumentQueryRepo` (`crates/dbflux_storage/src/repositories/qry_saved_document_queries.rs`) and `SavedDocumentQueryManager` (`crates/dbflux_ui_base/src/saved_document_query_manager.rs`). ### Language Services - `LanguageService` trait in `crates/dbflux_core/src/query/language_service.rs` exposes `validate`, `detect_dangerous`, and `editor_diagnostics`. `SqlLanguageService` is the default impl for relational drivers. - Non-SQL dialects ship their `LanguageService` from the driver crate (e.g. `TSqlLanguageService` lives in `dbflux_driver_mssql`; MongoDB and Redis dangerous detection live in their own driver crates and route through `classify_query_for_language(&QueryLanguage, &str)`). - `DangerousQueryKind` enumerates risky patterns across SQL (`DeleteNoWhere`, `UpdateNoWhere`, `Truncate`, `Drop`, `Alter`, `Script`), MongoDB (`deleteMany`, `updateMany`, `dropCollection`, `dropDatabase`), and Redis (`FlushAll`, `FlushDb`, `MultiDelete`, `KeysPattern`). - The UI must call into the dispatcher; do NOT add per-driver dangerous-query branches in `dbflux_ui`. ### WHERE Clause Syntax DBFlux MCP uses a unified JSON WHERE clause syntax that works across all database drivers (SQL, MongoDB, Redis, DynamoDB): **ColumnRef Pattern**: Column references support three forms: - `ColumnRef::Name("email")` — Simple column reference - `ColumnRef::Nested(vec!["metadata", "profile", "age"])` — Nested document field (MongoDB, JSONB) - `ColumnRef::JsonPath { column: "config", path: "$.notifications.email" }` — JSON path syntax **Operators**: Standard comparison (`$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$in`, `$nin`), pattern matching (`$like`, `$ilike`, `$regex`), NULL handling (`null`, `$eq: null`), existence (`$exists`), array operations (`$contains`, `$overlap`, `$size`, `$all`), and logical composition (`$and`, `$or`, `$not`). **Type Coercion**: Automatic type conversion (string ↔ number ↔ boolean) with validation. **Driver Translation**: WHERE clauses translate to SQL WHERE, MongoDB query filters, Redis SCAN patterns, or DynamoDB FilterExpression. **Reference**: See `crates/dbflux_mcp_server/docs/WHERE_CLAUSE_SYNTAX.md` for complete syntax guide with examples. ### DDL Preview System MCP provides a preview-before-execute workflow for schema changes: **Preview Workflow**: 1. AI agent calls `preview_mutation` with operation parameters 2. DBFlux generates SQL/query text or an execution preview using driver-owned generation/planning 3. Preview returned with SQL, classification, affected objects, and warnings 4. Agent reviews and decides whether to proceed 5. Agent calls actual tool (`alter_table`, `create_table`, etc.) if safe **Current limitation**: - DDL preview is not exposed as a standalone MCP tool in this branch. The old `preview_ddl` surface was removed because it could not guarantee a non-mutating preview across drivers. **Classification Algorithm**: `classify_alter_table_operation()` in `dbflux_core/src/query/classify.rs` determines risk level: - `ADD COLUMN` (nullable or with default) → `AdminSafe` - `ADD COLUMN` (non-nullable without default) → `Admin` (requires backfill) - `DROP COLUMN`, `RENAME COLUMN`, `ALTER COLUMN` → `Admin` - `ADD CONSTRAINT` (validation) → `AdminSafe` - `ADD CONSTRAINT` (FK with CASCADE DELETE) → `Admin` - `DROP CONSTRAINT`, `DROP INDEX` → `Admin` - `DROP TABLE`, `TRUNCATE TABLE`, `DROP DATABASE` → `AdminDestructive` **ALTER TABLE Safety Rules**: - Safe operations: `ADD COLUMN` (nullable), `CREATE INDEX`, validation constraints - Risky operations: `DROP COLUMN` (data loss), `RENAME COLUMN` (app breakage), `ALTER COLUMN` (type change) - Destructive operations: `DROP TABLE`, `TRUNCATE TABLE` **Driver-Specific Behavior**: - PostgreSQL: All DDL is transactional (except `CREATE INDEX CONCURRENTLY`) - MySQL: DDL is NOT transactional; rewrites entire table for most `ALTER TABLE` ops - SQLite: Limited `ALTER TABLE` support (only `ADD COLUMN`, `RENAME`); `DROP COLUMN` requires table recreation - SQL Server (MSSQL): DDL is transactional (can run inside a transaction and roll back) **Reference**: See `crates/dbflux_mcp_server/docs/DDL_SAFETY.md` for complete safety guide with classification matrix. ### Platform Detection `crates/dbflux_ui_base/src/platform.rs` handles X11/Wayland differences: - `floating_window_kind()` returns `WindowKind::Floating` on every platform. On Linux, gpui parents a floating window to the window holding keyboard focus when it opens (`xdg_toplevel.set_parent` on Wayland, `WM_TRANSIENT_FOR` on X11), so tiling compositors float it at its requested size. Do not use `WindowKind::Dialog` for secondary windows: it is modal and blocks input to the parent. - `apply_window_options()` sets min size so X11 WMs emit `WM_NORMAL_HINTS` A `pub use dbflux_ui_base::platform::*` shim remains at `crates/dbflux_ui/src/platform.rs` for internal compatibility. ### Release Channels & Branding - `ReleaseChannel` (`crates/dbflux_core/src/release_channel.rs`) derives the channel (`Stable` / `Rc` / `Nightly`) once from the compiled `CARGO_PKG_VERSION`. CI stamps the version before building, so the channel is baked into the binary. - Read channel identity through the accessors — `app_id()`, `display_name()`, `db_file_name()` — never branch on the raw version string and never hardcode `dbflux` / `dbflux-nightly` identifiers. Nightly returns a distinct `app_id` and DB file so it coexists with stable; `Rc` shares stable's identity. - The channel-aware DB path lives in `crates/dbflux_storage/src/paths.rs` (`dbflux_db_path`); nightly can opt into the stable DB via `set_nightly_shares_stable_db` / `nightly_shares_stable_db`. - Brand assets live under `resources/branding/{stable,nightly}/` (served per channel in `crates/dbflux_ui/src/assets.rs`). Packaging and Nix files substitute channel placeholders. The release/nightly flow is documented in `docs/RELEASE.md`. ## Website The site lives in `web/` and is an Astro static build. It is not a crate: `cargo` excludes it, and the Rust workflows skip changes confined to it or to markdown. **It renders the repository's own documentation.** `docs/*.md`, the driver READMEs, `ARCHITECTURE.md` and `CONTRIBUTING.md` are read straight out of git at build time. There is no second copy, so a behaviour change and the paragraph describing it belong in the same commit, and editing a document is all that is needed to change the site. **It publishes several versions.** `web/versions.json` lists them; each entry is a git ref, and the product version shown for it is read from that ref's `Cargo.toml` rather than typed. The first entry is the current release and is served unprefixed at `/docs/`; the rest are served under their id (`/v0.6/docs/`, `/nightly/docs/`) and keep those URLs. Granularity is the minor series, not the patch, because a release branch takes cherry-picked fixes only. Rules that matter when touching documentation: - Write markdown that reads correctly on GitHub. Relative links between documents and labels that name a file are rewritten to site routes at build time, so `[Settings](SETTINGS.md)` works in both places. - Diagrams go in ```mermaid fences. They render as diagrams on the site; ASCII art does not. - Screenshots live in `docs/images//` as WebP, one light and one dark file, and every locale shares them: `images/...` from `docs/`, `../images/...` from a translation. Embed them with `` so GitHub and the site both pick the theme's image: ```html The main window with a PostgreSQL table open ``` - Adding a document to `docs/` gives it a page automatically. Its place in the reading order is declared in `web/src/data/nav.ts`; unlisted documents still get a page and appear under "Not yet filed" on the documentation index. - Never send a reader to the repository for something the site renders. Link to the page. ```bash cd web pnpm install pnpm dev # local server pnpm build # static output in web/dist pnpm check # types — this is a real gate, not decoration pnpm format # prettier ``` ### Where the documentation is served `DOCS_MODE` decides, and it has three values: | Value | What it builds | Documentation URL | |---|---|---| | `embedded` (default) | everything, one origin | `/docs/usage/` | | `site` | landing pages only | `https://docs.dbflux.dev/usage/` | | `docs` | the documentation, at the root of its own host | `/usage/` | Local development uses the default, so `pnpm dev` still brings up the whole site with one command. A split deployment is two builds of this same source: ```bash pnpm build:site # DOCS_MODE=site pnpm build:docs # DOCS_MODE=docs ``` Override `SITE_ORIGIN` and `DOCS_ORIGIN` if the hostnames change. Nothing else in the source knows a URL: pages link through `docsUrl()` and `siteUrl()`, which is what lets one variable move an entire section of the site. Neither host serves a page that belongs to the other. A split build writes a Cloudflare `_redirects` file (`web/src/integrations/host-redirects.ts`) so every `/docs/...` URL the site once published answers `301` to the documentation host, and `/about/` on the documentation host answers `301` back. A moved page must answer with a status code rather than a 200 carrying a meta refresh: a 200 at a URL that moved reads, to a crawler or an agent, as a site still being rearranged. `web/scripts/check-links.mjs` runs in CI after the build and fails on an internal link that points at a page which is not built. Versions differ, so a link that resolves in nightly may not resolve in an older release. Two caches will mislead you. Astro keeps rendered markdown in `web/.astro` **and** `web/node_modules/.astro`, so a change to the markdown pipeline can appear to do nothing — use `pnpm clean`. And `astro dev` and `astro preview` are daemons that outlive the shell and silently redirect to an existing instance instead of starting a new one, so a stale server can serve an old build long after a rebuild; use `pnpm stop`. ## Common Pitfalls 1. Forgetting `cx.notify()` after state changes 2. Blocking UI thread — use `background_executor().spawn()` for DB ops 3. Entity updates in render loops — guard with `.take()` 4. Missing feature gates on driver code 5. Creating closures per cell in tables — use row-level handlers with hit-testing instead 6. Canvas re-rendering every frame — cache scroll state and only sync on meaningful changes 7. Audit event validation — `EventCategory::Config` requires `summary`, `object_type`, AND `object_id`; missing any causes silent failure in `validate_event()` For key files and the cross-crate map, see `ARCHITECTURE.md`.