docs: architecture map (initial 5-slice generation, 105 pages)
Five-slice navigable map of the entire codebase under
docs/architecture-map/. Each slice is a self-contained
breadcrumbed sub-tree:
01-frontend (16) Svelte/SvelteKit UI
02-tauri-runtime (26) src-tauri commands + lifecycle
03-audio-transcription (16) audio + transcription crates
04-llm-formatting-mcp (19) llm, ai-formatting, mcp, cloud
05-core-storage-hotkey-build core, storage, hotkey, workspace,
(26) CI, dev glue
Plus master README.md and data-flow-end-to-end.md tracing
audio bytes from microphone to FTS5 search to MCP read.
Generated by 5 parallel subagents on 2026/05/09 against
HEAD 3c47000. Each page has YAML frontmatter, file:line code
refs, sibling cross-links, plain-English summaries.
Aggregated debt surfaced (full lists in master README):
RB-08 macOS power assertion, schema head drift v14 vs v15,
VAD blocked on ort version conflict, streaming primitives
not wired into live.rs, no prompt versioning, MCP has no
auth, cloud-providers in-memory keystore, SettingsPage
2 484 LOC, commands/live.rs 1 737 LOC, dual theme system,
brand rename to Lumenote pending across the codebase.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
102
docs/architecture-map/05-core-storage-hotkey-build/README.md
Normal file
102
docs/architecture-map/05-core-storage-hotkey-build/README.md
Normal file
@@ -0,0 +1,102 @@
|
||||
---
|
||||
name: Slice 5 — Core, Storage, Hotkey, Build
|
||||
type: architecture-map-page
|
||||
slice: 05-core-storage-hotkey-build
|
||||
last_verified: 2026/05/09
|
||||
---
|
||||
|
||||
# Slice 5: Core, Storage, Hotkey, Build / CI
|
||||
|
||||
> **Where you are:** [Architecture map](../README.md) → Core, Storage, Hotkey, Build
|
||||
|
||||
**Plain English summary.** This slice is Magnotia's foundations. Three Rust crates the rest of the workspace builds on top of: `magnotia-core` (shared types, hardware probes, model registry, thread tuning), `magnotia-storage` (the SQLite database, FTS5 search, file-system paths), and `magnotia-hotkey` (a Wayland-friendly evdev hotkey listener for Linux). Plus the workspace-level glue that wires the Rust workspace together with the SvelteKit frontend: `Cargo.toml`, the GitHub Actions pipelines, the dev launcher, the static asset folder, and the package configuration files.
|
||||
|
||||
If a new engineer wants to know where a public type comes from, where a setting is persisted, or how a release artefact gets built, the answer is somewhere in this slice.
|
||||
|
||||
## At a glance
|
||||
|
||||
- **Crates:** three. `magnotia-core` (1,805 LOC), `magnotia-storage` (3,771 LOC), `magnotia-hotkey` (632 LOC). Total ~6,200 LOC.
|
||||
- **Workspace glue:** `Cargo.toml` (161 bytes), `package.json`, `vite.config.js`, `svelte.config.js`, `jsconfig.json`, `run.sh`, `static/`, `.gitignore`.
|
||||
- **CI:** three workflows — `check.yml` (per-push compile + lint + libs tests + frontend), `build.yml` (release-bundle build for tags + manual dispatch), `audit.yml` (weekly Mondays cargo-audit + npm-audit).
|
||||
- **Database head:** schema version **15** (commit on disk, supersedes the v14 figure quoted in `HANDOVER.md` 2026/04/25).
|
||||
- **Key external deps:** `sysinfo 0.35` (hardware), `libloading 0.8` (Vulkan loader probe), `sqlx 0.8` (no default features, runtime-tokio + sqlite only), `evdev 0.12` (Linux hotkeys), `notify 7` (device hotplug), `uuid 1` (v4, random).
|
||||
- **Targets:** Linux (primary, evdev backend), Windows, macOS. Hotkey crate is no-op on non-Linux platforms — Tauri's global-shortcut plugin handles those targets.
|
||||
- **Default profile UUID:** `00000000-0000-0000-0000-000000000001`. Created by migration v6, protected by SQL triggers from rename or delete.
|
||||
|
||||
## Map of this slice
|
||||
|
||||
`magnotia-core`:
|
||||
|
||||
- [Public types and enums (Segment, Transcript, Megabytes, ModelId, EngineName)](core-types-and-enums.md)
|
||||
- [Constants module (sample rate, VAD, RAM thresholds, chunk timing)](core-constants.md)
|
||||
- [Error type and Result alias](core-error.md)
|
||||
- [Hardware probe (sysinfo + CpuFeatures + Vulkan loader)](core-hardware-probe.md)
|
||||
- [Power-state probe (sysfs, 10s TTL cache, test override)](core-power.md)
|
||||
- [Inference thread tuning (Workload, battery halve, GPU clamp)](core-tuning.md)
|
||||
- [Model registry (Whisper + Parakeet entries with pinned SHA256)](core-model-registry.md)
|
||||
- [Recommendation scoring (rank_recommendations)](core-recommendation.md)
|
||||
- [Process-watch (meeting detection by process name)](core-process-watch.md)
|
||||
- [App paths (database, recordings, models, logs)](core-paths.md)
|
||||
|
||||
`magnotia-storage`:
|
||||
|
||||
- [Storage overview (sqlx config, init flow, default features rationale)](storage-overview.md)
|
||||
- [Schema and migrations (v1-v15 catalogue)](storage-schema-and-migrations.md)
|
||||
- [FTS5 transcript search](storage-fts5-search.md)
|
||||
- [Transcripts CRUD](storage-crud-transcripts.md)
|
||||
- [Tasks and subtasks CRUD](storage-crud-tasks.md)
|
||||
- [Profiles and profile_terms CRUD](storage-crud-profiles.md)
|
||||
- [Settings, error log, feedback, implementation rules](storage-crud-settings-and-misc.md)
|
||||
- [File storage paths (database, recordings, crashes, logs)](storage-file-paths.md)
|
||||
|
||||
`magnotia-hotkey`:
|
||||
|
||||
- [Linux evdev listener (devices, hotplug, modifiers, Pressed/Released)](hotkey-linux-evdev.md)
|
||||
|
||||
Workspace and build glue:
|
||||
|
||||
- [Workspace `Cargo.toml` (members, release profile)](workspace-cargo.md)
|
||||
- [CI: `check.yml`, `build.yml`, `audit.yml`](ci-pipeline.md)
|
||||
- [Dev launcher (`run.sh`) and root `package.json`](dev-launcher-and-scripts.md)
|
||||
- [Vite, SvelteKit, jsconfig](frontend-build-config.md)
|
||||
- [Static assets and `pcm-processor.js` worklet](static-assets.md)
|
||||
- [`HANDOVER.md` and root `README.md` pointer](dev-handover-pointer.md)
|
||||
|
||||
## How this slice connects to others
|
||||
|
||||
- **`magnotia-core` is the universal lower bound.** Every other crate in the workspace depends on it. The shape contract is: `Segment`, `Transcript`, `ModelId`, `EngineName`, `Megabytes`, `AudioSamples`, `MagnotiaError`, `Result`, plus `paths::AppPaths`. See [`core-types-and-enums.md`](core-types-and-enums.md).
|
||||
- **Slice 1 (frontend)** never imports any of these crates directly. It reaches them through Tauri commands (slice 2). The single setting key the frontend cares about is `magnotia_preferences` (a JSON blob), persisted via `magnotia_storage::get_setting` / `set_setting` (slice 5) called from the Tauri preferences command (slice 2).
|
||||
- **Slice 2 (Tauri runtime)** is the heaviest consumer. It calls `magnotia_storage::init` at startup, registers the SQLite pool as Tauri-managed state, and every command in `src-tauri/src/commands/*.rs` reaches into `magnotia_storage` for persistence and `magnotia_core::paths` for filesystem locations. The hotkey command at `src-tauri/src/commands/hotkey.rs` is the sole consumer of `magnotia_hotkey::EvdevHotkeyListener`.
|
||||
- **Slice 3 (audio + transcription)** consumes `magnotia_core::types::{AudioSamples, Segment, Transcript}` and `magnotia_core::constants::{WHISPER_SAMPLE_RATE, WHISPER_CHANNELS, PARAKEET_*, CHUNK_INTERVAL_MS}`. The transcription engines also consume `magnotia_core::tuning::inference_thread_count(Workload::Whisper, gpu_offloaded)` for thread sizing.
|
||||
- **Slice 4 (LLM + formatting + MCP)** consumes `magnotia_core::tuning::{inference_thread_count, Workload::Llm}`, `magnotia_core::paths::AppPaths::llm_models_dir()`, `magnotia_core::types::Segment` (formatting), and `magnotia_core::constants::SMART_PARAGRAPH_GAP_SECS` (formatting). The MCP server is the only consumer of `magnotia_storage::init_readonly` — it opens the DB read-only so no MCP tool can mutate user data even if the dispatcher misroutes a request.
|
||||
|
||||
## Existing in-repo docs
|
||||
|
||||
These are the critical-issue write-ups from the 2026-04-22 review that overlap with slice 5. Cross-referenced from the relevant per-page file. **Not duplicated here.**
|
||||
|
||||
- [`docs/issues/c3-migrations-atomicity.md`](../../issues/c3-migrations-atomicity.md) — drove the v9-onwards transactional migration design. See [`storage-schema-and-migrations.md`](storage-schema-and-migrations.md).
|
||||
- [`docs/issues/c4-transcript-profile-fk.md`](../../issues/c4-transcript-profile-fk.md) — drove migration v9, the table rebuild that landed `transcripts.profile_id` with a real foreign key. See [`storage-schema-and-migrations.md`](storage-schema-and-migrations.md).
|
||||
- [`docs/issues/keystore-thread-safety.md`](../../issues/keystore-thread-safety.md) — keystore lives in `crates/cloud-providers/` (slice 4) but the lesson generalises to every shared mutable state in `magnotia-core`. Cross-referenced from the tuning page.
|
||||
- [`docs/issues/hotkey-linux-device-filter.md`](../../issues/hotkey-linux-device-filter.md) — the RB-12 hard-coded `KEY_A` / `KEY_R` filter that the current `device_supports_combo` replaced. See [`hotkey-linux-evdev.md`](hotkey-linux-evdev.md).
|
||||
- [`docs/issues/power-assertion-macos-objc2.md`](../../issues/power-assertion-macos-objc2.md) — RB-08, the only open MAJOR per `HANDOVER.md`. The implementation lives in slice 2 (`src-tauri`) but the API design lives near `magnotia-core` conceptually. Linked from the slice debt section.
|
||||
- [`docs/audit/phase0-cartography.md`](../../audit/phase0-cartography.md) — phase-0 codebase cartography, predates this map but covers crate-graph topology in summary form.
|
||||
- [`docs/code-review-2026-04-22.md`](../../code-review-2026-04-22.md) — full audit. Slice 5 work items: RB-08 (macOS power assertion), RB-12 (hotkey filter, fixed), C3 (migrations atomicity, fixed), C4 (transcript profile FK, fixed). Several MAJORs across the slice 2 / 3 / 4 surface area too — see the document.
|
||||
|
||||
## Open questions, debt, drift
|
||||
|
||||
- **RB-08 still open.** macOS power assertion (`crates/.../power_assertion.rs` — surface lives in slice 2) is a non-functional stub awaiting the `objc2` rewrite plus runtime verification on Apple silicon. Per `HANDOVER.md`, gates v0.1 tagging. Tracked in [`docs/issues/power-assertion-macos-objc2.md`](../../issues/power-assertion-macos-objc2.md).
|
||||
- **Schema head drift in human-facing docs.** `HANDOVER.md` (2026/04/25) says v14; the migration registry on disk is v15 (commit 2026/05/09 added `idx_transcripts_profile_created`). Worth a single sweep of human-facing handovers when the next session opens.
|
||||
- **Hotkey is Linux-only at runtime.** macOS and Windows fall back to Tauri's global-shortcut plugin (slice 2). The crate compiles cleanly on all three OSes (the stub module is a no-op), but feature parity across platforms is provided by two different mechanisms. A single user-visible inconsistency: per-device hotplug. The Tauri plugin doesn't have it.
|
||||
- **Power probe is Linux-only.** macOS / Windows return `PowerState::Unknown`, which callers treat as `OnAc`. Native probes (`IOPSGetProvidingPowerSourceType`, `GetSystemPowerStatus`) are noted-deferred in [`core-power.md`](core-power.md).
|
||||
- **GPU probe is a stub.** `magnotia_core::hardware::probe_gpu()` returns `None`. CPU probe is wired (sysinfo + CPUID), Vulkan loader is probed via `libloading`, but the actual GPU vendor / VRAM are never populated. Recommendation scoring still works because it inspects `Some(gpu).acceleration` and gracefully scores zero-bonus when `None`. See [`core-hardware-probe.md`](core-hardware-probe.md).
|
||||
- **`migration_v15` test exists but no migration v15 reverse path.** Migrations are append-only; the registry's contract is forward-only and idempotent. Reverting requires a fresh DB or a `DROP INDEX` migration v16. The contract is documented in [`storage-schema-and-migrations.md`](storage-schema-and-migrations.md).
|
||||
- **Default profile is enforced by SQL triggers, not by code.** `00000000-0000-0000-0000-000000000001` cannot be deleted or renamed, but if a future migration drops or alters the trigger names (`trg_protect_default_profile_delete`, `trg_protect_default_profile_rename`), the protection silently disappears. No regression test asserts the triggers still exist on the head schema; today the integration tests only assert the protection at DML time.
|
||||
- **Code-signing not configured.** Both macOS (Apple Developer ID) and Windows (code-signing certificate) are commented out in `build.yml`. Users hit Gatekeeper / SmartScreen on first run. Documented in [`ci-pipeline.md`](ci-pipeline.md).
|
||||
- **`run.sh` hard-codes a Fedora-style `LIBCLANG_PATH`.** The launcher exports `/usr/lib64/llvm21/lib64`, which is correct on Jake's Monolith but breaks on a Debian/Ubuntu dev box. Worth softening to a `LIBCLANG_PATH=${LIBCLANG_PATH:-/usr/lib64/llvm21/lib64}` form.
|
||||
|
||||
## See also
|
||||
|
||||
- [Slice 1 — Frontend](../01-frontend/README.md)
|
||||
- [Slice 2 — Tauri runtime](../02-tauri-runtime/README.md)
|
||||
- [Slice 3 — Audio + transcription](../03-audio-transcription/README.md)
|
||||
- [Slice 4 — LLM, formatting, MCP](../04-llm-formatting-mcp/README.md)
|
||||
@@ -0,0 +1,162 @@
|
||||
---
|
||||
name: CI pipeline
|
||||
type: architecture-map-page
|
||||
slice: 05-core-storage-hotkey-build
|
||||
last_verified: 2026/05/09
|
||||
---
|
||||
|
||||
# CI pipeline
|
||||
|
||||
> **Where you are:** [Architecture map](../README.md) → [Core, Storage, Hotkey, Build](README.md) → CI pipeline
|
||||
|
||||
**Plain English summary.** Three GitHub Actions workflows. `check.yml` runs per-push compile + lint + library tests + frontend build on Linux, Windows, and macOS. `build.yml` produces installer artefacts (.AppImage/.deb on Linux, .msi/.exe on Windows, .dmg/.app on macOS) for tag pushes and on-demand. `audit.yml` runs cargo-audit and npm-audit weekly.
|
||||
|
||||
## At a glance
|
||||
|
||||
- Files: `.github/workflows/check.yml`, `build.yml`, `audit.yml`.
|
||||
- LOC: 7,017 + 7,157 + 1,443 bytes respectively.
|
||||
- Shared concerns across check and build: same Linux / macOS / Windows system-package install steps (libwebkit2gtk, Vulkan SDK, libclang for bindgen), same `Swatinem/rust-cache@v2` cache scoping (`workspaces: .` because the workspace target dir is `./target`, **not `src-tauri/target`** — this was the silent miss that made Windows checks slow).
|
||||
|
||||
## `check.yml` — per-push compile + lint + tests
|
||||
|
||||
### Triggers
|
||||
|
||||
```yaml
|
||||
on: [push, pull_request]
|
||||
concurrency:
|
||||
group: check-${{ github.ref }}
|
||||
cancel-in-progress: true # newer push supersedes older
|
||||
```
|
||||
|
||||
### Jobs
|
||||
|
||||
#### `rust` — matrix
|
||||
|
||||
```yaml
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-22.04, windows-latest, macos-latest]
|
||||
```
|
||||
|
||||
Steps per OS:
|
||||
|
||||
1. **Install system deps** — libwebkit2gtk-4.1, libappindicator3, librsvg2, libasound2, libudev, patchelf, cmake, build-essential, libclang, clang, libvulkan, glslang-tools, spirv-tools (Linux); Homebrew vulkan-headers, vulkan-loader, molten-vk, shaderc, llvm (macOS); choco llvm + vulkan-sdk (Windows). Each install resolves `LIBCLANG_PATH` / `VULKAN_SDK` dynamically so a minor SDK version bump does not hardcode-break the step.
|
||||
2. `dtolnay/rust-toolchain@stable` with `rustfmt, clippy`.
|
||||
3. `Swatinem/rust-cache@v2` with `workspaces: .` and shared key `magnotia-${{ matrix.os }}`.
|
||||
4. `cargo check --workspace --all-targets`.
|
||||
5. `cargo fmt --all -- --check`.
|
||||
6. `cargo clippy --workspace --all-targets -- -D warnings`.
|
||||
7. `cargo test --workspace --lib` (Linux only, gated on `matrix.os == 'ubuntu-22.04'`).
|
||||
8. `cargo audit` (Linux only).
|
||||
|
||||
The Linux-only gating on tests + audit keeps macOS and Windows legs focused on compile coverage. The library-tests-only flag (`--lib`) excludes integration tests that need a runtime / GPU.
|
||||
|
||||
#### `frontend` — Linux only
|
||||
|
||||
1. Setup Node 20.
|
||||
2. `npm ci`.
|
||||
3. `npm audit --audit-level=high`.
|
||||
4. `npm run build` — Vite-only build (no `tauri build`).
|
||||
5. `svelte-check` for type and template errors.
|
||||
|
||||
### Why the workspace cache scope matters
|
||||
|
||||
The original CI step pointed `Swatinem/rust-cache@v2` at `workspaces: src-tauri`. The workspace target dir is at `./target` (defined by the repo-root `Cargo.toml`), not `src-tauri/target`. That meant the cache silently missed every run, and Windows check runs felt like they recompiled `sqlx` from scratch every time. Documented inline at `check.yml`. The current scope `workspaces: .` is the fix.
|
||||
|
||||
## `build.yml` — release artefacts
|
||||
|
||||
### Triggers
|
||||
|
||||
```yaml
|
||||
on:
|
||||
push:
|
||||
tags: ['v*'] # tagged releases (v0.1.0, v0.2.0, ...)
|
||||
workflow_dispatch: # manual, any branch
|
||||
inputs:
|
||||
tag_name:
|
||||
description: 'Optional tag name to attach the build to'
|
||||
required: false
|
||||
```
|
||||
|
||||
`workflow_dispatch` lets us build a Windows binary on demand to dual-boot test without cutting a release.
|
||||
|
||||
### Concurrency
|
||||
|
||||
```yaml
|
||||
concurrency:
|
||||
group: build-${{ github.ref }}
|
||||
cancel-in-progress: false # release builds finish even on tag re-pushes
|
||||
```
|
||||
|
||||
Different from `check.yml`: a release build is too expensive to abandon mid-flight.
|
||||
|
||||
### Matrix
|
||||
|
||||
```yaml
|
||||
include:
|
||||
- os: ubuntu-22.04 artefacts: *.AppImage, *.deb
|
||||
- os: windows-latest artefacts: *.msi, *.exe
|
||||
- os: macos-latest artefacts: *.dmg, *.app
|
||||
```
|
||||
|
||||
### Steps
|
||||
|
||||
1. System deps (same as `check.yml`).
|
||||
2. Rust toolchain.
|
||||
3. Cache (`shared-key: magnotia-build-${{ matrix.os }}`, distinct from check.yml's key so the build cache is not invalidated by check runs).
|
||||
4. `npm ci`.
|
||||
5. `tauri-apps/tauri-action@v0` — runs `tauri build`. On tag pushes, attaches artefacts to a draft GitHub Release. Empty `tagName` for `workflow_dispatch` so we get artefacts only.
|
||||
6. `actions/upload-artifact@v4` — always uploads to the run page (30-day retention) so the workflow_dispatch path produces something downloadable too.
|
||||
7. `du -h` on the produced bundle directory for visibility.
|
||||
|
||||
### Code signing
|
||||
|
||||
**Not configured.** Both macOS (Apple Developer ID) and Windows (code-signing certificate) signing blocks are commented out in `build.yml`. Users hit Gatekeeper / SmartScreen on first run.
|
||||
|
||||
To enable later:
|
||||
|
||||
- **macOS:** uncomment the `APPLE_SIGNING_IDENTITY` / `APPLE_CERTIFICATE` / `APPLE_CERTIFICATE_PASSWORD` env block; add the corresponding repo secrets.
|
||||
- **Windows:** uncomment the `WINDOWS_CERTIFICATE` / `WINDOWS_CERTIFICATE_PASSWORD` env block; add the secrets.
|
||||
|
||||
Documented in the file header.
|
||||
|
||||
## `audit.yml` — weekly vulnerability scan
|
||||
|
||||
### Triggers
|
||||
|
||||
```yaml
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 6 * * 1" # Mondays 06:00 UTC
|
||||
workflow_dispatch:
|
||||
```
|
||||
|
||||
Mondays so any advisory has the whole week to be triaged rather than landing on a Friday.
|
||||
|
||||
### Jobs
|
||||
|
||||
#### `cargo-audit`
|
||||
|
||||
Uses `rustsec/audit-check@v2` against the RustSec advisory DB. Fails on any unignored advisory.
|
||||
|
||||
#### `npm-audit`
|
||||
|
||||
`npm audit --audit-level=high`. Ignores low / moderate noise — only high and critical advisories warrant a bump.
|
||||
|
||||
### Why a separate workflow?
|
||||
|
||||
A newly published advisory surfaces as its own failing run (easy to spot, easy to track) without blocking unrelated PR work. The same `cargo audit` runs as part of `check.yml` on every push, so this workflow is the catch-all for advisories that land between pushes.
|
||||
|
||||
## Watch-outs
|
||||
|
||||
- **`cargo audit` runs in two places.** `check.yml` (Linux only, on every push) and `audit.yml` (weekly). The duplication is cheap and useful.
|
||||
- **`tauri-action@v0` is unpinned.** `@v0` floats. A breaking change in tauri-action would surface on the next release attempt. Worth pinning to a specific version when v0.1 ships.
|
||||
- **macOS `LIBCLANG_PATH` resolution.** `brew --prefix llvm` resolves to the Apple Silicon path on M-series runners (`/opt/homebrew/...`) and the Intel path on intel runners (`/usr/local/...`). The dynamic resolution handles both.
|
||||
- **Linux runners use Ubuntu 22.04, not 24.04.** Pinned because 22.04's `libwebkit2gtk-4.1` package is stable; 24.04's namespace shifted. Worth re-evaluating once GitHub Actions retires 22.04.
|
||||
- **No nightly clippy.** The `-D warnings` clippy step uses stable. A nightly canary job would catch upcoming lint changes earlier.
|
||||
|
||||
## See also
|
||||
|
||||
- [Workspace Cargo.toml](workspace-cargo.md) — the release profile this CI consumes.
|
||||
- [Dev launcher and scripts](dev-launcher-and-scripts.md) — local equivalent.
|
||||
@@ -0,0 +1,113 @@
|
||||
---
|
||||
name: Core constants
|
||||
type: architecture-map-page
|
||||
slice: 05-core-storage-hotkey-build
|
||||
last_verified: 2026/05/09
|
||||
---
|
||||
|
||||
# Core constants
|
||||
|
||||
> **Where you are:** [Architecture map](../README.md) → [Core, Storage, Hotkey, Build](README.md) → Core constants
|
||||
|
||||
**Plain English summary.** A flat list of `pub const` values that other crates reach for so the same magic numbers (sample rate, mel-spectrogram dimensions, RAM thresholds, chunk timing) are not duplicated. If you change one of these, the change propagates by recompile to every consumer.
|
||||
|
||||
## At a glance
|
||||
|
||||
- File: `crates/core/src/constants.rs` (38 LOC).
|
||||
- All items are `pub const` with concrete primitive types.
|
||||
- No external deps.
|
||||
- Consumers: slice 3 (transcription, audio capture), slice 4 (formatting paragraph break heuristic), `core::types::AudioSamples::mono_16khz` (slice 5 internal).
|
||||
|
||||
## What's in here
|
||||
|
||||
### Audio pipeline (Whisper baseline)
|
||||
|
||||
```rust
|
||||
pub const WHISPER_SAMPLE_RATE: u32 = 16_000; // crates/core/src/constants.rs:2
|
||||
pub const WHISPER_CHANNELS: u16 = 1; // crates/core/src/constants.rs:3
|
||||
```
|
||||
|
||||
Used by `AudioSamples::mono_16khz`, the audio capture worklet target rate (cross-link to [`static-assets.md`](static-assets.md)), and the Whisper engine (slice 3). The frontend `pcm-processor.js` worklet uses literal `16000` rather than importing this constant, so a change here without a coordinated worklet edit produces silently wrong audio. Documented in [`static-assets.md`](static-assets.md).
|
||||
|
||||
### Parakeet mel spectrogram (`crates/core/src/constants.rs:6-12`)
|
||||
|
||||
```rust
|
||||
pub const PARAKEET_N_FFT: usize = 512;
|
||||
pub const PARAKEET_HOP_LENGTH: usize = 160;
|
||||
pub const PARAKEET_WIN_LENGTH: usize = 400;
|
||||
pub const PARAKEET_N_MELS: usize = 80;
|
||||
pub const PARAKEET_PRE_EMPHASIS: f32 = 0.97;
|
||||
pub const PARAKEET_BLANK_TOKEN: usize = 1024;
|
||||
pub const PARAKEET_LOG_GUARD: f32 = 5.960_464_5e-8; // 2^-24
|
||||
```
|
||||
|
||||
These match the Parakeet TDT 0.6B v2 ONNX export's expected feature shape. Consumed by the Parakeet engine in slice 3. Touch only in lock-step with the model file revision pinned in [`core-model-registry.md`](core-model-registry.md).
|
||||
|
||||
### Live chunk timing (`crates/core/src/constants.rs:14-16`)
|
||||
|
||||
```rust
|
||||
pub const CHUNK_INTERVAL_MS: u64 = 3000;
|
||||
pub const MIN_CHUNK_SAMPLES: usize = 8000; // 0.5 s at 16 kHz
|
||||
```
|
||||
|
||||
Used by the live-session loop (slice 3). 3-second chunks at 16 kHz mean 48 000-sample buffers per inference call. The 8 000-sample minimum prevents very short final chunks producing garbage transcriptions when a session ends mid-buffer.
|
||||
|
||||
### Smart paragraph break (`crates/core/src/constants.rs:18-19`)
|
||||
|
||||
```rust
|
||||
pub const SMART_PARAGRAPH_GAP_SECS: f64 = 2.0;
|
||||
```
|
||||
|
||||
Consumed by `magnotia_ai_formatting::pipeline` (slice 4). When the gap between adjacent `Segment`s exceeds 2 seconds, a paragraph break is inserted. Cross-link: [Slice 4 formatting pipeline](../04-llm-formatting-mcp/README.md).
|
||||
|
||||
### History limits (`crates/core/src/constants.rs:21-22`)
|
||||
|
||||
```rust
|
||||
pub const HISTORY_MAX_ENTRIES: usize = 100;
|
||||
```
|
||||
|
||||
Used by the history page query path (slice 1 / slice 2). Today the storage layer pages with explicit `limit` arguments rather than reading this constant directly — the constant is a soft cap consumed by the frontend.
|
||||
|
||||
### RAM thresholds (`crates/core/src/constants.rs:24-28`)
|
||||
|
||||
```rust
|
||||
pub const RAM_MINIMUM_FOR_LOCAL_STT: f64 = 2.0; // GB
|
||||
pub const RAM_THRESHOLD_LIGHTWEIGHT: f64 = 4.0;
|
||||
pub const RAM_THRESHOLD_STANDARD: f64 = 8.0;
|
||||
pub const RAM_THRESHOLD_COMFORTABLE: f64 = 16.0;
|
||||
```
|
||||
|
||||
Used by the recommendation scoring path ([`core-recommendation.md`](core-recommendation.md)) and the runtime-capabilities banner (slice 2). The "comfortable" threshold drives the +10 score bonus.
|
||||
|
||||
### VAD configuration (`crates/core/src/constants.rs:30-35`)
|
||||
|
||||
```rust
|
||||
pub const VAD_SPEECH_THRESHOLD: f64 = 0.5;
|
||||
pub const VAD_MIN_SPEECH_DURATION_MS: u32 = 250;
|
||||
pub const VAD_MAX_SPEECH_DURATION_S: u32 = 30;
|
||||
pub const VAD_MIN_SILENCE_DURATION_MS: u32 = 300;
|
||||
pub const VAD_SPEECH_PAD_MS: u32 = 100;
|
||||
```
|
||||
|
||||
Defaults for the Silero VAD wrapper (slice 3). User-tunable via settings; these are the cold-start values.
|
||||
|
||||
### Model download (`crates/core/src/constants.rs:37-38`)
|
||||
|
||||
```rust
|
||||
pub const DOWNLOAD_CHUNK_BYTES: usize = 65_536; // 64 KiB
|
||||
```
|
||||
|
||||
The chunk size at which the resumable HTTP downloader emits `DownloadProgress` events. 64 KiB is fine for both the Parakeet ONNX shards and the Whisper GGML files. Consumed by the model manager paths in slices 3 and 4.
|
||||
|
||||
## Watch-outs
|
||||
|
||||
- **No grouping or namespacing.** Everything is flat. Discoverability via `grep` against the constant names.
|
||||
- **No serialisation.** These are compile-time constants. Changing one requires a rebuild; persisted user data is unaffected.
|
||||
- **`PARAKEET_BLANK_TOKEN = 1024` is the model's vocabulary index.** Hard-coded to the 0.6B v2 export. A different Parakeet variant would use a different blank index — coordinate with the registry.
|
||||
|
||||
## See also
|
||||
|
||||
- [Public types and enums](core-types-and-enums.md)
|
||||
- [Model registry](core-model-registry.md)
|
||||
- [Recommendation scoring](core-recommendation.md)
|
||||
- [Static assets and `pcm-processor.js`](static-assets.md)
|
||||
@@ -0,0 +1,64 @@
|
||||
---
|
||||
name: Core error type
|
||||
type: architecture-map-page
|
||||
slice: 05-core-storage-hotkey-build
|
||||
last_verified: 2026/05/09
|
||||
---
|
||||
|
||||
# Core error type
|
||||
|
||||
> **Where you are:** [Architecture map](../README.md) → [Core, Storage, Hotkey, Build](README.md) → Core error type
|
||||
|
||||
**Plain English summary.** A single structured error enum every Magnotia crate uses, plus a `Result<T>` alias. Errors serialise to JSON so the frontend can switch behaviour on the variant rather than parse strings.
|
||||
|
||||
## At a glance
|
||||
|
||||
- File: `crates/core/src/error.rs` (60 LOC).
|
||||
- External deps: `thiserror 2`, `serde 1`.
|
||||
- Re-exported as `magnotia_core::{MagnotiaError, Result}`.
|
||||
- Consumers: every workspace crate. Tauri commands wrap their internal errors in `MagnotiaError` so the frontend (slice 1) sees a stable JSON shape.
|
||||
|
||||
## What's in here
|
||||
|
||||
### `MagnotiaError` — `crates/core/src/error.rs:11`
|
||||
|
||||
`#[derive(Debug, thiserror::Error, Serialize)]` enum:
|
||||
|
||||
| Variant | Display string | Notes |
|
||||
|---|---|---|
|
||||
| `ModelNotFound(ModelId)` | `model not found: {0}` | Used by registry lookups and the model manager. |
|
||||
| `ModelNotDownloaded(ModelId)` | `model not downloaded: {0}` | Distinct from `ModelNotFound`: model is registered, files missing. |
|
||||
| `EngineNotLoaded` | `engine not loaded: call load_model first` | Engine API ordering violation. |
|
||||
| `TranscriptionFailed(String)` | `transcription failed: {0}` | Wraps Whisper / Parakeet / Moonshine engine errors. |
|
||||
| `AudioDecodeFailed(String)` | `audio decode failed: {0}` | Symphonia / hound failures (slice 3). |
|
||||
| `AudioCaptureFailed(String)` | `audio capture failed: {0}` | cpal / native capture failures (slice 3). |
|
||||
| `DownloadFailed(String)` | `model download failed: {0}` | Resumable download errors. |
|
||||
| `FileNotFound(PathBuf)` | `file not found: {<displayed>}` | `PathBuf::display()` interpolated. |
|
||||
| `StorageError(String)` | `storage error: {0}` | sqlx, profile FK violations, migration failures. |
|
||||
| `Io(std::io::Error)` | `io error: {0}` | `#[from]` so `?`-conversion from `std::io::Error` is automatic. |
|
||||
| `Other(String)` | `{0}` | Catch-all bucket. |
|
||||
|
||||
### `Result<T>` — `crates/core/src/error.rs:60`
|
||||
|
||||
```rust
|
||||
pub type Result<T> = std::result::Result<T, MagnotiaError>;
|
||||
```
|
||||
|
||||
Every public function in the workspace that can fail returns `magnotia_core::Result<T>` rather than `std::result::Result<T, MagnotiaError>` so the imports stay short.
|
||||
|
||||
## Data flow / contract
|
||||
|
||||
- All variants are `Serialize`-able. `std::io::Error` does not derive `Serialize`, so the `Io` variant uses a custom `serialize_with` adaptor (`serialize_io_error` at `crates/core/src/error.rs:53`) that emits the error's `Display` string.
|
||||
- Variants do not carry source-location information. If you need a stack-style trace, attach context at the call site by wrapping in `StorageError(format!("{action} failed: {e}"))` — the storage CRUD layer follows this convention universally.
|
||||
- Tauri serialises the enum verbatim. The frontend can switch on the discriminant by reading the JSON tag (the variant name).
|
||||
|
||||
## Watch-outs
|
||||
|
||||
- **No `From<sqlx::Error>` impl.** The storage crate manually converts every sqlx error to `MagnotiaError::StorageError(format!(...))`. Adding an automatic `From` would let raw sqlx error strings leak into the frontend; the explicit map step is intentional.
|
||||
- **No `Source` chain.** `thiserror` would let you wrap source errors in fields with `#[source]` for chained `Display`. Today every wrapped error is flattened to `String` to keep the JSON shape simple.
|
||||
- **`Other(String)` is a leaky bucket.** New error categories should get their own variant rather than reaching for `Other`. Audit `Other` usage if the error log starts hiding distinct failure modes behind the same string.
|
||||
|
||||
## See also
|
||||
|
||||
- [Public types and enums](core-types-and-enums.md)
|
||||
- [Storage CRUD surfaces](storage-overview.md)
|
||||
@@ -0,0 +1,124 @@
|
||||
---
|
||||
name: Core hardware probe
|
||||
type: architecture-map-page
|
||||
slice: 05-core-storage-hotkey-build
|
||||
last_verified: 2026/05/09
|
||||
---
|
||||
|
||||
# Core hardware probe
|
||||
|
||||
> **Where you are:** [Architecture map](../README.md) → [Core, Storage, Hotkey, Build](README.md) → Core hardware probe
|
||||
|
||||
**Plain English summary.** What CPU, RAM, GPU, and operating system the user has. The probe runs once at app startup so the recommendation engine can pick the right speech model. CPU feature flags (AVX2, FMA, NEON) are also surfaced so the runtime can warn users on pre-AVX2 silicon that performance will be a fraction of expected.
|
||||
|
||||
## At a glance
|
||||
|
||||
- File: `crates/core/src/hardware.rs` (261 LOC).
|
||||
- External deps: `sysinfo 0.35`, `libloading 0.8` (Vulkan probe).
|
||||
- Public surface: `SystemProfile`, `CpuInfo`, `CpuFeatures`, `GpuInfo`, `GpuVendor`, `GpuAcceleration`, `Os`, `probe_system`, `probe_cpu_features`, `probe_gpu`, `probe_os`, `vulkan_loader_available`.
|
||||
- Consumers: slice 2 (runtime-capabilities banner), [`core-recommendation.md`](core-recommendation.md), slices 3 + 4 (`vulkan_loader_available` to gate Vulkan acceleration).
|
||||
|
||||
## What's in here
|
||||
|
||||
### `SystemProfile` — `crates/core/src/hardware.rs:7`
|
||||
|
||||
```rust
|
||||
pub struct SystemProfile {
|
||||
pub ram: Megabytes,
|
||||
pub cpu: CpuInfo,
|
||||
pub gpu: Option<GpuInfo>,
|
||||
pub os: Os,
|
||||
}
|
||||
```
|
||||
|
||||
Top-level snapshot. Built by `probe_system()`.
|
||||
|
||||
### `CpuInfo` and `CpuFeatures` — `crates/core/src/hardware.rs:14, 30`
|
||||
|
||||
```rust
|
||||
pub struct CpuInfo {
|
||||
pub logical_processors: usize,
|
||||
pub brand: String,
|
||||
pub features: CpuFeatures,
|
||||
}
|
||||
pub struct CpuFeatures {
|
||||
pub avx2: bool,
|
||||
pub avx512f: bool,
|
||||
pub fma: bool,
|
||||
pub sse4_2: bool,
|
||||
pub neon: bool,
|
||||
}
|
||||
```
|
||||
|
||||
`CpuFeatures::has_ggml_baseline` (`hardware.rs:41`) returns `true` when the CPU has the baseline that `ggml` (whisper.cpp / llama.cpp) expects: `avx2 && fma` on x86_64, `neon` on aarch64. When `false`, the runtime banner fires (slice 2). Reference: `whisper-rs` issues #8 and #117 (illegal instruction on pre-AVX2 CPUs).
|
||||
|
||||
### `probe_cpu_features()` — `crates/core/src/hardware.rs:59`
|
||||
|
||||
`std::is_x86_feature_detected!` lowers to runtime CPUID. On aarch64, NEON is the architectural baseline so it is always reported as `true`. On other targets all flags are `false`.
|
||||
|
||||
### GPU types — `crates/core/src/hardware.rs:84-105`
|
||||
|
||||
```rust
|
||||
pub struct GpuInfo {
|
||||
pub vendor: GpuVendor,
|
||||
pub vram: Megabytes,
|
||||
pub acceleration: GpuAcceleration,
|
||||
}
|
||||
pub enum GpuVendor { Nvidia, Amd, Intel, Apple, Unknown }
|
||||
pub struct GpuAcceleration { pub cuda: bool, pub metal: bool, pub vulkan: bool }
|
||||
```
|
||||
|
||||
### `Os` — `crates/core/src/hardware.rs:107`
|
||||
|
||||
```rust
|
||||
pub enum Os { Windows, Linux, MacOs, Ios, Android }
|
||||
```
|
||||
|
||||
Resolved at runtime via `cfg!(target_os = ...)` in `probe_os()`. Unsupported targets default to `Linux`.
|
||||
|
||||
### `probe_system()` — `crates/core/src/hardware.rs:162`
|
||||
|
||||
The composed probe. Builds a single `sysinfo::System::new_all()` (which is expensive on Windows and macOS), then derives RAM, CPU, GPU, and OS from it. Calling the individual probes one-by-one would re-walk `/proc` per call.
|
||||
|
||||
### `probe_gpu()` — `crates/core/src/hardware.rs:135`
|
||||
|
||||
**Stub.** Returns `None` today. The intended implementation routes through `wgpu` for vendor / VRAM detection and a platform-specific path (NVML on NVIDIA, Metal API on macOS) for acceleration flags. See [Open questions](#open-questions).
|
||||
|
||||
### `vulkan_loader_available()` — `crates/core/src/hardware.rs:183`
|
||||
|
||||
Best-effort probe for the Vulkan loader shared library. Whisper.cpp and llama.cpp Vulkan backends silently fall back to CPU if the loader is missing at runtime. `libloading::Library::new` on candidates:
|
||||
|
||||
- Linux: `libvulkan.so.1`, `libvulkan.so`
|
||||
- Windows: `vulkan-1.dll`
|
||||
- macOS: `libvulkan.1.dylib`, `libMoltenVK.dylib`
|
||||
|
||||
A successful open means the loader is resolvable. Moved from `src-tauri/src/commands/models.rs` so non-Tauri crates (transcription, llm) can call it without depending on the Tauri binary.
|
||||
|
||||
## Tests
|
||||
|
||||
`crates/core/src/hardware.rs:206-261`:
|
||||
|
||||
- `probe_cpu_features_runs_without_panicking` — smoke.
|
||||
- `probe_system_populates_cpu_features` — asserts `CpuFeatures` is wired into the profile.
|
||||
- `ggml_baseline_matches_x86_64_rule` — verifies the `avx2 && fma` rule on x86, falls through gracefully on non-x86.
|
||||
- `ggml_baseline_requires_both_avx2_and_fma` — negative test (avx2 alone insufficient).
|
||||
- `vulkan_loader_available_does_not_panic` — smoke; cannot assert the boolean, depends on host.
|
||||
|
||||
## Watch-outs
|
||||
|
||||
- **`probe_gpu()` returns `None`.** Recommendation scoring still works because `score_model` checks `if let Some(gpu) = ...` and gracefully scores zero-bonus when missing. But every accelerator-aware code path is conservative until this is implemented.
|
||||
- **`probe_system()` is expensive on Windows.** ~10-50 ms on the cold path because `System::new_all()` queries every WMI category. Call once at startup, cache the result in Tauri-managed state.
|
||||
- **`vulkan_loader_available()` calls `unsafe { libloading::Library::new }`.** Safe by audit: the handle is dropped at the end of the iteration without any symbol calls. Documented inline at `hardware.rs:194`.
|
||||
|
||||
## Open questions
|
||||
|
||||
- **GPU probe is a stub.** Tracked in the slice debt section of [`README.md`](README.md). Until it lands, no code path can distinguish NVIDIA / AMD / Intel / Apple at runtime.
|
||||
- **No PCI / DRM enumeration on Linux.** `wgpu::Adapter::get_info` would give us vendor + name on every supported platform but pulls a heavy dep. Worth weighing against a smaller crate (`pci-ids`?) for the Linux-only path.
|
||||
|
||||
## See also
|
||||
|
||||
- [Constants module](core-constants.md)
|
||||
- [Power-state probe](core-power.md)
|
||||
- [Inference thread tuning](core-tuning.md)
|
||||
- [Model registry](core-model-registry.md)
|
||||
- [Recommendation scoring](core-recommendation.md)
|
||||
@@ -0,0 +1,114 @@
|
||||
---
|
||||
name: Core model registry
|
||||
type: architecture-map-page
|
||||
slice: 05-core-storage-hotkey-build
|
||||
last_verified: 2026/05/09
|
||||
---
|
||||
|
||||
# Core model registry
|
||||
|
||||
> **Where you are:** [Architecture map](../README.md) → [Core, Storage, Hotkey, Build](README.md) → Core model registry
|
||||
|
||||
**Plain English summary.** The static catalogue of every speech-to-text model Magnotia ships or knows how to download. One Parakeet ONNX model and six Whisper GGML variants, each with pinned Hugging Face revisions and SHA256 digests so a downloaded file can be verified bit-for-bit against the registry. Pure data — the recommendation scoring lives in [`core-recommendation.md`](core-recommendation.md).
|
||||
|
||||
## At a glance
|
||||
|
||||
- File: `crates/core/src/model_registry.rs` (247 LOC).
|
||||
- External deps: standard library only (`LazyLock`).
|
||||
- Public surface: `Engine`, `SpeedTier`, `AccuracyTier`, `LanguageSupport`, `ModelFile`, `ModelEntry`, `all_models`, `find_model`.
|
||||
- Consumers: slice 2 model commands, the model manager, the recommendation engine ([`core-recommendation.md`](core-recommendation.md)), the runtime-capabilities banner, the frontend model picker.
|
||||
|
||||
## What's in here
|
||||
|
||||
### Enums — `crates/core/src/model_registry.rs:7-36`
|
||||
|
||||
```rust
|
||||
pub enum Engine { Whisper, Parakeet, Moonshine }
|
||||
pub enum SpeedTier { Instant, Fast, Moderate, Slow }
|
||||
pub enum AccuracyTier { Excellent, Great, Good }
|
||||
pub enum LanguageSupport { EnglishOnly, Multilingual(u16) }
|
||||
```
|
||||
|
||||
`Moonshine` is reserved (no live entries today). `LanguageSupport::Multilingual(u16)` carries the count of supported languages but only the variant is used in scoring.
|
||||
|
||||
### `ModelFile` — `crates/core/src/model_registry.rs:39`
|
||||
|
||||
```rust
|
||||
pub struct ModelFile {
|
||||
pub filename: &'static str,
|
||||
pub url: &'static str,
|
||||
pub size: Megabytes,
|
||||
pub sha256: &'static str, // hex, length 64
|
||||
}
|
||||
```
|
||||
|
||||
Every URL pins a specific Hugging Face revision SHA. The registry's test (`model_registry.rs:222-246`) asserts that no URL contains `/resolve/main/` and that every digest is exactly 64 hex characters.
|
||||
|
||||
### `ModelEntry` — `crates/core/src/model_registry.rs:50`
|
||||
|
||||
```rust
|
||||
pub struct ModelEntry {
|
||||
pub id: ModelId,
|
||||
pub engine: Engine,
|
||||
pub display_name: &'static str,
|
||||
pub disk_size: Megabytes,
|
||||
pub ram_required: Megabytes,
|
||||
pub speed_tier: SpeedTier,
|
||||
pub accuracy_tier: AccuracyTier,
|
||||
pub languages: LanguageSupport,
|
||||
pub files: Vec<ModelFile>,
|
||||
pub description: &'static str,
|
||||
}
|
||||
```
|
||||
|
||||
### `ALL_MODELS` — `crates/core/src/model_registry.rs:63`
|
||||
|
||||
A `LazyLock<Vec<ModelEntry>>`. Seven entries:
|
||||
|
||||
| ID | Engine | Disk | RAM | Speed | Accuracy | Notes |
|
||||
|---|---|---|---|---|---|---|
|
||||
| `parakeet-ctc-0.6b-int8` | Parakeet | 650 MB | 700 MB | Instant | Great | Four ONNX shards. English only. |
|
||||
| `whisper-tiny-en` | Whisper | 75 MB | 390 MB | Fast | Good | Bundled with the app. |
|
||||
| `whisper-base-en` | Whisper | 142 MB | 500 MB | Fast | Good | Sweet spot for low-RAM. |
|
||||
| `whisper-small-en` | Whisper | 466 MB | 1024 MB | Moderate | Great | Accuracy-first. |
|
||||
| `whisper-distil-small-en` | Whisper | 336 MB | 900 MB | Fast | Great | Distilled, ~6× faster than `small`. |
|
||||
| `whisper-medium-en` | Whisper | 1500 MB | 2600 MB | Slow | Excellent | Best Whisper accuracy. |
|
||||
| `whisper-distil-large-v3` | Whisper | 1550 MB | 2800 MB | Moderate | Excellent | Near `large-v3` accuracy. |
|
||||
|
||||
All entries are English-only at present. Multilingual variants would slot into `languages` cleanly.
|
||||
|
||||
### Public functions
|
||||
|
||||
```rust
|
||||
pub fn all_models() -> &'static [ModelEntry]; // model_registry.rs:208
|
||||
pub fn find_model(id: &ModelId) -> Option<&'static ModelEntry>; // model_registry.rs:213
|
||||
```
|
||||
|
||||
Both return references into the `LazyLock`. No allocation.
|
||||
|
||||
## Data flow / contract
|
||||
|
||||
- Pure data. Lookups are read-only.
|
||||
- The Parakeet entry's four files are fetched by the model downloader as a directory of `<model_id>/` siblings (see [`core-paths.md`](core-paths.md)). Whisper entries are single GGML files.
|
||||
- The `description` field is user-facing copy. British-English spelling enforced.
|
||||
- SHA256 verification happens in the model downloader (slice 3), not here.
|
||||
|
||||
## Tests
|
||||
|
||||
`crates/core/src/model_registry.rs:217-246`:
|
||||
|
||||
- `every_model_file_has_sha256_and_pinned_url` — guard against accidental drift. SHA must be 64 hex chars; URL must not contain `/resolve/main/`.
|
||||
|
||||
## Watch-outs
|
||||
|
||||
- **Hugging Face revisions can be force-pushed.** The pinned revision SHAs in the URLs are content addresses on HF's git LFS, but a maintainer with admin rights can in theory rewrite history. The local SHA256 verification at download time catches this. There is no automated CI check that re-fetches and verifies; verification happens lazily on user-facing downloads.
|
||||
- **`description` is `&'static str`.** No i18n. If we localise the model picker we will need a key plus a separate string table.
|
||||
- **Adding a new model requires a recompile.** No runtime registry override path. Conscious choice — keeps the surface area small and the SHA256 invariants meaningful.
|
||||
- **`Moonshine` is in the enum but no entries.** Compiler does not complain because the enum is `non_exhaustive`-shaped via match arms inside scoring (see [`core-recommendation.md`](core-recommendation.md)). When a Moonshine entry lands, the scoring path needs an explicit accelerator branch.
|
||||
|
||||
## See also
|
||||
|
||||
- [Public types and enums (Megabytes, ModelId)](core-types-and-enums.md)
|
||||
- [Recommendation scoring](core-recommendation.md)
|
||||
- [App paths (`speech_model_dir`)](core-paths.md)
|
||||
- [Slice 3 model manager](../03-audio-transcription/README.md)
|
||||
@@ -0,0 +1,86 @@
|
||||
---
|
||||
name: Core app paths
|
||||
type: architecture-map-page
|
||||
slice: 05-core-storage-hotkey-build
|
||||
last_verified: 2026/05/09
|
||||
---
|
||||
|
||||
# Core app paths
|
||||
|
||||
> **Where you are:** [Architecture map](../README.md) → [Core, Storage, Hotkey, Build](README.md) → Core app paths
|
||||
|
||||
**Plain English summary.** Where Magnotia stores its files on disk. One root, derived from the OS conventions (LOCALAPPDATA on Windows, `~/Library/Application Support/Magnotia` on macOS, `$XDG_DATA_HOME/magnotia` on Linux), and named accessor methods for each subdirectory. Every other crate that needs to know "where does the database live?" reaches for `magnotia_core::paths::AppPaths::current()`.
|
||||
|
||||
## At a glance
|
||||
|
||||
- File: `crates/core/src/paths.rs` (125 LOC).
|
||||
- External deps: standard library only.
|
||||
- Public surface: `AppPaths` (struct), `app_paths`, `app_data_dir`.
|
||||
- Consumers: `magnotia-storage::file_storage` (re-exports the path helpers), the Tauri startup code (slice 2), the model manager (slices 3 + 4), the diagnostic-report bundler (slice 2).
|
||||
|
||||
## What's in here
|
||||
|
||||
### `AppPaths` — `crates/core/src/paths.rs:6`
|
||||
|
||||
```rust
|
||||
pub struct AppPaths { app_data_dir: PathBuf }
|
||||
|
||||
impl AppPaths {
|
||||
pub fn current() -> Self; // resolves at construction
|
||||
pub fn app_data_dir(&self) -> PathBuf;
|
||||
pub fn database_path(&self) -> PathBuf; // <root>/magnotia.db
|
||||
pub fn recordings_dir(&self) -> PathBuf; // <root>/recordings
|
||||
pub fn crashes_dir(&self) -> PathBuf; // <root>/crashes
|
||||
pub fn logs_dir(&self) -> PathBuf; // <root>/logs
|
||||
pub fn diagnostic_reports_dir(&self) -> PathBuf; // <root>/diagnostic-reports
|
||||
pub fn models_dir(&self) -> PathBuf; // <root>/models
|
||||
pub fn speech_model_dir(&self, id: &ModelId) -> PathBuf; // <root>/models/<id>
|
||||
pub fn llm_models_dir(&self) -> PathBuf; // <root>/models/llm
|
||||
pub fn migration_sentinel(&self, name: &str) -> PathBuf; // <root>/.<name>.sentinel
|
||||
}
|
||||
|
||||
pub fn app_paths() -> AppPaths;
|
||||
pub fn app_data_dir() -> PathBuf;
|
||||
```
|
||||
|
||||
### Root resolution — `crates/core/src/paths.rs:66`
|
||||
|
||||
The `resolve_app_data_dir` function picks the root by `cfg(target_os = ...)`:
|
||||
|
||||
| OS | Root |
|
||||
|---|---|
|
||||
| Windows | `%LOCALAPPDATA%/magnotia` |
|
||||
| macOS | `$HOME/Library/Application Support/Magnotia` |
|
||||
| Linux | `$HOME/.magnotia` if it already exists (legacy), else `$XDG_DATA_HOME/magnotia` if set, else `$HOME/.local/share/magnotia` |
|
||||
| other | `$HOME/.magnotia` |
|
||||
|
||||
The Linux legacy-path branch keeps existing users on `~/.magnotia` (early dogfooding default) without forcing a migration when the canonical XDG location is preferred.
|
||||
|
||||
### Sentinel files — `crates/core/src/paths.rs:53`
|
||||
|
||||
`migration_sentinel(name) -> <root>/.<name>.sentinel`. Used for one-shot data migrations outside the SQLite schema (for example, a one-off file-system reorganisation). The pattern is: write the sentinel after the migration runs successfully; check for the sentinel on next startup; skip the migration if the sentinel exists.
|
||||
|
||||
## Data flow / contract
|
||||
|
||||
- All paths are derived from a single root. Changing `resolve_app_data_dir` once moves every downstream file. Tested at `paths.rs:111`.
|
||||
- Path resolution is pure: only reads env vars (`HOME`, `LOCALAPPDATA`, `XDG_DATA_HOME`) plus `Path::exists` for the Linux legacy probe.
|
||||
- `AppPaths::current()` does not create directories. Callers that need a directory to exist call `std::fs::create_dir_all` at write time. The storage `init` function does this for the database parent (`crates/storage/src/database.rs:10`).
|
||||
|
||||
## Tests
|
||||
|
||||
`crates/core/src/paths.rs:104-125`:
|
||||
|
||||
- `derives_all_paths_from_one_base` — verifies that `database_path`, `speech_model_dir(<id>)`, and `llm_models_dir` are all rooted at the same `app_data_dir`.
|
||||
|
||||
## Watch-outs
|
||||
|
||||
- **`std::env::var(...).unwrap_or_else(|_| "/tmp".to_string())` is the fallback for missing `HOME`.** Should never trigger in practice; defensive.
|
||||
- **`AppPaths::current()` reads env vars at every call.** Cheap, but repeated calls are wasteful. Slice 2 caches a `OnceLock<AppPaths>` so the value is resolved once at startup.
|
||||
- **No Windows fallback for missing `LOCALAPPDATA`.** Falls through to `.` (current working directory). On a misconfigured Windows host this could write the database next to the binary. Not great; the impact is limited to first-run scenarios where the env is broken.
|
||||
- **Sentinel files are hidden on Unix (`.<name>.sentinel`) but visible on Windows.** Acceptable; sentinels live alongside `magnotia.db` so the user sees both.
|
||||
|
||||
## See also
|
||||
|
||||
- [Storage file paths](storage-file-paths.md) — re-exports.
|
||||
- [Model registry](core-model-registry.md) — model IDs feed into `speech_model_dir`.
|
||||
- [Slice 2 Tauri startup](../02-tauri-runtime/README.md) — caller of `AppPaths::current()` at boot.
|
||||
@@ -0,0 +1,95 @@
|
||||
---
|
||||
name: Core power-state probe
|
||||
type: architecture-map-page
|
||||
slice: 05-core-storage-hotkey-build
|
||||
last_verified: 2026/05/09
|
||||
---
|
||||
|
||||
# Core power-state probe
|
||||
|
||||
> **Where you are:** [Architecture map](../README.md) → [Core, Storage, Hotkey, Build](README.md) → Core power-state probe
|
||||
|
||||
**Plain English summary.** Reports whether the machine is on AC or battery so callers can drop thread counts and skip GPU offload when energy matters more than throughput. Linux uses the documented `/sys/class/power_supply/` ABI. macOS and Windows return `Unknown` for now.
|
||||
|
||||
## At a glance
|
||||
|
||||
- File: `crates/core/src/power.rs` (328 LOC, 124 of which are tests).
|
||||
- External deps: standard library only (sysfs is plain text). Tests use `tempfile` (dev-dep).
|
||||
- Public surface: `PowerState`, `parse_power_state_from_dir`, `probe_power_state`. Plus `with_override` and `force_clear_cache` / `force_set_cache` as `pub(crate)` test helpers.
|
||||
- Consumers: [`core-tuning.md`](core-tuning.md). The runtime-capabilities banner in slice 2 may also surface the state.
|
||||
|
||||
## What's in here
|
||||
|
||||
### `PowerState` — `crates/core/src/power.rs:15`
|
||||
|
||||
```rust
|
||||
pub enum PowerState { OnAc, OnBattery, Unknown }
|
||||
```
|
||||
|
||||
`Unknown` is treated as `OnAc` by callers, preserving today's pre-clamp behaviour on platforms where the probe cannot fire.
|
||||
|
||||
### `parse_power_state_from_dir` — `crates/core/src/power.rs:40`
|
||||
|
||||
Pure function. Walks a `/sys/class/power_supply/`-style directory and applies these rules (matching the kernel's documented sysfs ABI):
|
||||
|
||||
1. Any entry with `type` in {`Mains`, `USB`} and `online == 1` → `OnAc`.
|
||||
2. Else any entry with `type == Battery` → `OnBattery`.
|
||||
3. Else → `Unknown`.
|
||||
|
||||
Top-level failures (missing dir, unreadable supply_dir) return `Unknown` without panicking. Per-entry failures are silently skipped.
|
||||
|
||||
### `probe_power_state()` — `crates/core/src/power.rs:115`
|
||||
|
||||
Resolution order, highest to lowest priority:
|
||||
|
||||
1. **In-process test override** (set via `with_override`). Test-only, never compiled into release builds.
|
||||
2. **`MAGNOTIA_POWER_STATE_OVERRIDE` env var** — `ac` | `battery` | `unknown`, case-insensitive. Used by the `thread_sweep.rs` integration tests.
|
||||
3. **Linux:** `parse_power_state_from_dir("/sys/class/power_supply")`.
|
||||
4. **macOS / Windows / other:** `Unknown`.
|
||||
|
||||
Both override paths bypass the cache so tests always see the value they set.
|
||||
|
||||
### TTL cache — `crates/core/src/power.rs:75-99, 124-136`
|
||||
|
||||
```rust
|
||||
const POWER_STATE_TTL: Duration = Duration::from_secs(10);
|
||||
```
|
||||
|
||||
Result is cached in a `Mutex<Option<CachedState>>` for 10 seconds. Caching prevents the inference thread-tuning helper from calling sysfs on every inference call (~10 syscalls per probe; called every chunk).
|
||||
|
||||
### Test helpers — `crates/core/src/power.rs:88, 93, 188`
|
||||
|
||||
- `force_clear_cache()` — `pub(crate)` — drops the cache slot.
|
||||
- `force_set_cache(state)` — `pub(crate)` — pre-populates the cache slot.
|
||||
- `with_override<R>(state, body) -> R` — `pub(crate)` — sets `TEST_OVERRIDE` for the duration of `body`. Holds a dedicated `TEST_LOCK` so override-using unit tests run serially even when cargo runs the test binary multi-threaded. `OverrideGuard` resets the override on drop, so a panicking test body cannot leak stale state.
|
||||
|
||||
All three helpers are `#[cfg(test)]`-gated and never compiled into release.
|
||||
|
||||
## Data flow / contract
|
||||
|
||||
- `probe_power_state` is the only public entry point production callers use.
|
||||
- The cache TTL means a power-source change takes up to 10 s to take effect. Acceptable for thread tuning; not adequate for a UI battery indicator.
|
||||
- Cache mutex poisoning is treated as a panic via `.expect("power cache mutex poisoned")`. A panicking holder of this mutex is already a bug; the loud failure mode is on purpose.
|
||||
|
||||
## Tests
|
||||
|
||||
11 tests in `crates/core/src/power.rs:202-327`:
|
||||
|
||||
- `power_state_variants_are_distinct` — sanity.
|
||||
- `parses_mains_online_as_on_ac` / `parses_battery_only_as_on_battery` / `parses_usb_pd_online_as_on_ac` — happy paths.
|
||||
- `parses_empty_dir_as_unknown` / `parses_missing_dir_as_unknown` / `parses_malformed_files_as_unknown_gracefully` — failure paths.
|
||||
- `override_drives_battery` / `override_drives_ac` / `override_drives_unknown` — in-process override.
|
||||
- `env_var_override_battery_via_set_var` — env-var override under the same `TEST_LOCK`.
|
||||
- `env_var_override_garbage_falls_through` — invalid env values fall through to the platform probe.
|
||||
- `ttl_cache_returns_cached_value_within_window` / `ttl_cache_clears_via_force_clear` — cache invariants.
|
||||
|
||||
## Watch-outs
|
||||
|
||||
- **macOS and Windows return `Unknown` always.** Native probes (`IOPSGetProvidingPowerSourceType` on macOS, `GetSystemPowerStatus` on Windows) are deferred. Consumers must treat `Unknown` as `OnAc` or behaviour will silently halve thread counts on every non-Linux machine.
|
||||
- **Per-entry sysfs read failures are silent.** The `read_trimmed().unwrap_or_default()` pattern means a permission-denied `online` file in a `Mains` entry would read as the empty string and the supply would be skipped. On a stuck-AC laptop where Mains was the unreadable entry, the function would return `OnBattery`. Documented in the function's doc comment at `power.rs:31`. Sysfs entries are world-readable in practice.
|
||||
- **`TEST_OVERRIDE` is `static Mutex<Option<PowerState>>`.** Process-global, test-only. Production builds do not compile it because of `#[cfg(test)]`.
|
||||
|
||||
## See also
|
||||
|
||||
- [Inference thread tuning](core-tuning.md) — the only production caller.
|
||||
- [Hardware probe](core-hardware-probe.md) — sibling.
|
||||
@@ -0,0 +1,70 @@
|
||||
---
|
||||
name: Core process-watch (meeting detection)
|
||||
type: architecture-map-page
|
||||
slice: 05-core-storage-hotkey-build
|
||||
last_verified: 2026/05/09
|
||||
---
|
||||
|
||||
# Core process-watch
|
||||
|
||||
> **Where you are:** [Architecture map](../README.md) → [Core, Storage, Hotkey, Build](README.md) → Core process-watch
|
||||
|
||||
**Plain English summary.** A lightweight "is the user in a meeting right now?" probe. One signal only: poll the running-process list and match user-editable substrings. No mic-activity heuristic, no calendar integration. If the user has opted in, Magnotia surfaces a non-modal toast so they can choose to start recording. The app never starts recording on its own from this signal.
|
||||
|
||||
## At a glance
|
||||
|
||||
- File: `crates/core/src/process_watch.rs` (123 LOC, ~40 of which are tests).
|
||||
- External deps: `sysinfo 0.35`.
|
||||
- Public surface: `ProcessLister` (struct), `list_running_process_names`, `match_meeting_patterns`.
|
||||
- Consumers: slice 2 (a Tauri command holds a long-lived `ProcessLister` behind `Mutex` and polls every 15 s).
|
||||
|
||||
## What's in here
|
||||
|
||||
### `ProcessLister` — `crates/core/src/process_watch.rs:20`
|
||||
|
||||
```rust
|
||||
pub struct ProcessLister { system: System }
|
||||
impl ProcessLister {
|
||||
pub fn new() -> Self;
|
||||
pub fn snapshot(&mut self) -> Vec<String>; // lowercased exe names
|
||||
}
|
||||
impl Default for ProcessLister { ... }
|
||||
```
|
||||
|
||||
Reusable wrapper around a `sysinfo::System`. The system is created once with `RefreshKind::nothing().with_processes(ProcessRefreshKind::nothing())` and refreshed in place via `refresh_processes(ProcessesToUpdate::All, true)` on every call. On a busy host (~300 processes), `System::new_with_specifics` followed by `refresh_processes` walks `/proc` cold and costs ~50–100 ms; reusing the same instance reuses sysinfo's per-process bookkeeping so subsequent refreshes are dominated by diffing rather than allocation.
|
||||
|
||||
### `list_running_process_names()` — `crates/core/src/process_watch.rs:59`
|
||||
|
||||
Convenience function. Allocates a fresh `ProcessLister` per call. Hot paths should hold a long-lived `ProcessLister` instead.
|
||||
|
||||
### `match_meeting_patterns(process_names, patterns) -> Vec<String>` — `crates/core/src/process_watch.rs:67`
|
||||
|
||||
Pure function. Case-insensitive substring match. Returns the set of patterns that matched at least once, in input order, deduped. Empty / whitespace-only patterns are skipped so a stray blank entry in the user's list never matches everything.
|
||||
|
||||
## Data flow / contract
|
||||
|
||||
- The slice-2 command holds the `ProcessLister` in `tauri::State` behind a `Mutex` and polls every 15 seconds.
|
||||
- Pattern list comes from user settings (the UI surfaces it as a comma-separated text box).
|
||||
- Match output drives a non-modal toast in the frontend. Magnotia never starts recording itself — the user decides.
|
||||
|
||||
## Tests
|
||||
|
||||
4 tests in `crates/core/src/process_watch.rs:84-122`:
|
||||
|
||||
- `matches_are_case_insensitive_substrings` — happy path with `Zoom Meeting` / `Microsoft Teams` / `firefox`.
|
||||
- `empty_and_whitespace_patterns_are_ignored` — guard.
|
||||
- `matches_are_deduped` — duplicate `zoom` patterns produce one match.
|
||||
- `list_running_returns_something_on_this_host` — smoke against the test runner's own process list.
|
||||
|
||||
## Watch-outs
|
||||
|
||||
- **`refresh_processes(ProcessesToUpdate::All, true)` is still ~5–10 ms even reused.** OK at 15 s cadence, would not be at sub-second cadence.
|
||||
- **Substring match is broad.** The pattern `"zoom"` matches a process named `zoomies` (a hypothetical screensaver). User-configurable so the user owns the false positive.
|
||||
- **Lowercased once, searched many times.** `process_names` are lowercased at snapshot time so the matcher itself can stay simple. If we ever want regex matching we will lose this optimisation.
|
||||
- **Linux / macOS / Windows process-name conventions differ.** `Zoom` on macOS, `zoom` on Linux, `Zoom.exe` on Windows. The case-insensitive substring match handles the case differences cleanly. The `.exe` suffix on Windows is fine because the pattern is a substring.
|
||||
- **Privacy.** Process names can leak metadata (other apps the user runs). The probe runs locally and the results never leave the machine, but it is a class of data to be careful with.
|
||||
|
||||
## See also
|
||||
|
||||
- [Hardware probe](core-hardware-probe.md) — sibling probe.
|
||||
- [Slice 2 Tauri commands](../02-tauri-runtime/README.md) for the meeting-detection command that owns the long-lived `ProcessLister`.
|
||||
@@ -0,0 +1,90 @@
|
||||
---
|
||||
name: Core recommendation scoring
|
||||
type: architecture-map-page
|
||||
slice: 05-core-storage-hotkey-build
|
||||
last_verified: 2026/05/09
|
||||
---
|
||||
|
||||
# Core recommendation scoring
|
||||
|
||||
> **Where you are:** [Architecture map](../README.md) → [Core, Storage, Hotkey, Build](README.md) → Core recommendation scoring
|
||||
|
||||
**Plain English summary.** Given a `SystemProfile` (RAM, CPU, GPU, OS), score every model in the registry and rank them. The top entry is what Magnotia recommends. No boolean flags or scattered "is recommended" markers — position in the ranked list **is** the recommendation.
|
||||
|
||||
## At a glance
|
||||
|
||||
- File: `crates/core/src/recommendation.rs` (197 LOC, 113 of which are tests).
|
||||
- External deps: standard library only.
|
||||
- Public surface: `ScoredModel`, `score_model`, `rank_recommendations`.
|
||||
- Consumers: slice 2 model commands (frontend exposes the ranked list), the model picker UI (slice 1).
|
||||
|
||||
## What's in here
|
||||
|
||||
### `ScoredModel` — `crates/core/src/recommendation.rs:7`
|
||||
|
||||
```rust
|
||||
pub struct ScoredModel {
|
||||
pub entry: &'static ModelEntry,
|
||||
pub score: f64,
|
||||
pub reason: String,
|
||||
}
|
||||
```
|
||||
|
||||
Borrows the registry entry by `'static` reference (no allocation per call). `reason` is a user-facing explanatory string, prefilled with `model.description` if no override applied.
|
||||
|
||||
### `score_model(model, profile) -> Option<ScoredModel>` — `crates/core/src/recommendation.rs:15`
|
||||
|
||||
Pure function. Returns `None` when the model exceeds the system's RAM budget. Otherwise computes:
|
||||
|
||||
| Component | Score |
|
||||
|---|---|
|
||||
| `SpeedTier::Instant` | +40 |
|
||||
| `SpeedTier::Fast` | +30 |
|
||||
| `SpeedTier::Moderate` | +20 |
|
||||
| `SpeedTier::Slow` | +10 |
|
||||
| `AccuracyTier::Excellent` | +30 |
|
||||
| `AccuracyTier::Great` | +20 |
|
||||
| `AccuracyTier::Good` | +10 |
|
||||
| GPU acceleration available for this model's engine | +15 |
|
||||
| Headroom > 4 GB above `model.ram_required` | +10 |
|
||||
|
||||
GPU acceleration matrix (`recommendation.rs:36-49`):
|
||||
|
||||
- **Whisper**: any of `metal`, `vulkan`, `cuda`.
|
||||
- **Parakeet** / **Moonshine**: `cuda` or `vulkan`.
|
||||
|
||||
When GPU acceleration applies, `reasons.push("GPU accelerated on your system")`. Otherwise `reason = model.description.to_string()`.
|
||||
|
||||
### `rank_recommendations(profile) -> Vec<ScoredModel>` — `crates/core/src/recommendation.rs:71`
|
||||
|
||||
Filters out registry entries that exceed RAM, scores the rest, sorts descending by score, returns the vector. `partial_cmp` falls through to `Ordering::Equal` if NaN appears (defensive; the scoring path can't produce NaN today).
|
||||
|
||||
## Data flow / contract
|
||||
|
||||
- Pure function over `&SystemProfile` and the `&'static [ModelEntry]` from the registry.
|
||||
- Order is fully determined by score, with ties broken by registry order (which is what `sort_by` preserves).
|
||||
- The "Parakeet first when fits" expectation is asserted by a test at `recommendation.rs:184`: any machine with enough RAM for Parakeet sees Parakeet at index 0.
|
||||
|
||||
## Tests
|
||||
|
||||
6 tests in `crates/core/src/recommendation.rs:85-197`. Test fixtures `profile_with_ram` and `profile_with_gpu` build minimal `SystemProfile`s.
|
||||
|
||||
- `score_model_excludes_models_exceeding_available_ram` — RAM budget guard.
|
||||
- `score_model_includes_models_fitting_in_ram` — happy path.
|
||||
- `score_model_boosts_gpu_accelerated_models` — GPU bonus is real.
|
||||
- `rank_recommendations_places_highest_score_first` — sort invariant.
|
||||
- `rank_recommendations_returns_empty_for_very_low_ram` — degenerate case.
|
||||
- `parakeet_is_top_recommendation_when_hardware_supports_it` — asserts the implicit policy that English-speaking users on capable hardware see Parakeet first because it beats Whisper on English at lower latency.
|
||||
|
||||
## Watch-outs
|
||||
|
||||
- **No CPU-feature gate.** A pre-AVX2 CPU does not down-rank Whisper or Parakeet entries. The runtime-capabilities banner (slice 2) handles that user-facing warning. Worth considering whether a hard down-rank ought to live here too.
|
||||
- **Recommendation ignores download cost.** A user on a slow connection still sees `whisper-distil-large-v3` ranked first because it scores 30+30+10 = 70 against Parakeet's 40+20+10 = 70 (tie, registry order picks Parakeet). On a 4 GB-RAM machine, only `whisper-base-en` and `whisper-tiny-en` survive RAM filtering, so the ordering is well-behaved on low-end hardware.
|
||||
- **GPU scoring keys off the `Engine` variant, not the model size.** A 75 MB Whisper Tiny on a Vulkan GPU still gets the +15 bonus, which is technically correct (the inference does run on GPU) but is a marginal preference signal at that size.
|
||||
- **`reason` is `String`, not a structured enum.** UI that wants to badge the reason ("GPU accelerated", "Best for your RAM") needs to parse the string today. Worth pivoting to a discriminated union when more reasons land.
|
||||
|
||||
## See also
|
||||
|
||||
- [Hardware probe (`SystemProfile`)](core-hardware-probe.md)
|
||||
- [Model registry](core-model-registry.md)
|
||||
- [Constants module (RAM thresholds)](core-constants.md)
|
||||
@@ -0,0 +1,95 @@
|
||||
---
|
||||
name: Core inference thread tuning
|
||||
type: architecture-map-page
|
||||
slice: 05-core-storage-hotkey-build
|
||||
last_verified: 2026/05/09
|
||||
---
|
||||
|
||||
# Core inference thread tuning
|
||||
|
||||
> **Where you are:** [Architecture map](../README.md) → [Core, Storage, Hotkey, Build](README.md) → Core inference thread tuning
|
||||
|
||||
**Plain English summary.** A single helper, `inference_thread_count`, that answers "how many CPU threads should we give whisper.cpp / llama.cpp on this user's machine right now?" Combines the physical-core budget, battery awareness, and GPU-offload awareness into one number. Logs the chosen value once per (workload, battery, GPU-offload) tuple so the same configuration does not flood the journal.
|
||||
|
||||
## At a glance
|
||||
|
||||
- File: `crates/core/src/tuning.rs` (233 LOC, 127 of which are tests).
|
||||
- External deps: `num_cpus 1`, `tracing 0.1`, plus the in-crate `power` module.
|
||||
- Public surface: `MIN_INFERENCE_THREADS`, `MAX_INFERENCE_THREADS`, `Workload` enum, `inference_thread_count`.
|
||||
- Consumers: slice 3 (Whisper engine, Parakeet engine — `Workload::Whisper`); slice 4 (LLM engine — `Workload::Llm`).
|
||||
|
||||
## What's in here
|
||||
|
||||
### Constants — `crates/core/src/tuning.rs:11, 20`
|
||||
|
||||
```rust
|
||||
pub const MIN_INFERENCE_THREADS: usize = 2;
|
||||
pub const MAX_INFERENCE_THREADS: usize = 8;
|
||||
```
|
||||
|
||||
Single-threaded inference is measurably worse than two on every multi-core part. Past 8 threads, `whisper.cpp` and `llama.cpp` scaling flattens because SMT siblings contend for the FPU during F16 / F32 matmul. 8 is a conservative ceiling that leaves <5% on the table for big-iron desktops while keeping consumer 6c/12t laptops out of contention territory.
|
||||
|
||||
### `Workload` enum — `crates/core/src/tuning.rs:23`
|
||||
|
||||
```rust
|
||||
pub enum Workload { Llm, Whisper }
|
||||
```
|
||||
|
||||
The two workloads have different floors when GPU offload is on.
|
||||
|
||||
### Internal floors — `crates/core/src/tuning.rs:33-34`
|
||||
|
||||
```rust
|
||||
const GPU_FLOOR_LLM: usize = 2;
|
||||
const GPU_FLOOR_WHISPER: usize = 4;
|
||||
```
|
||||
|
||||
Whisper retains CPU work even with full Vulkan offload (mel spectrogram, decoder bookkeeping, beam search). LLM (llama-style transformer) drops to near-zero CPU work when fully offloaded. Architectural invariant tested at `tuning.rs:200`: `GPU_FLOOR_WHISPER >= GPU_FLOOR_LLM`.
|
||||
|
||||
### `inference_thread_count(workload, gpu_offloaded) -> usize` — `crates/core/src/tuning.rs:53`
|
||||
|
||||
Resolution order:
|
||||
|
||||
1. **`MAGNOTIA_INFERENCE_THREADS=N`** — absolute bypass, returns `N` without any clamps.
|
||||
2. **Base** = `num_cpus::get_physical()`. Falls back to `std::thread::available_parallelism()`, then to `MIN_INFERENCE_THREADS = 2`.
|
||||
3. **On battery** → `base /= 2`. Power state is read from `magnotia_core::power::probe_power_state()`. The 10-second cache there means the call is cheap; see [`core-power.md`](core-power.md).
|
||||
4. **GPU offloaded** → clamp to `GPU_FLOOR_<workload>`. Whisper floor is 4, LLM floor is 2.
|
||||
5. **Final clamp** to `[MIN_INFERENCE_THREADS, MAX_INFERENCE_THREADS]`.
|
||||
|
||||
### One-shot logging — `crates/core/src/tuning.rs:39, 87-101`
|
||||
|
||||
A `static SEEN: OnceLock<Mutex<HashSet<(Workload, bool, bool)>>>` records every distinct `(workload, on_battery, gpu_offloaded)` tuple the helper has been called with this process. The first time a tuple is seen, the helper logs at `INFO` via `tracing!` with the chosen thread count and the active clamps. Subsequent calls with the same tuple are silent. Without this guard the journal would accumulate one INFO per inference chunk.
|
||||
|
||||
## Data flow / contract
|
||||
|
||||
- Pure read; no I/O beyond reading the env var (cheap) and probing power (cached 10 s).
|
||||
- Idempotent within a tuple. Different tuples each get one INFO line.
|
||||
- The env override is intentional: it lets users on unusual silicon (32-core Threadripper with a noisy thermal design, big.LITTLE asymmetric cores, etc) override the heuristic.
|
||||
|
||||
## Tests
|
||||
|
||||
8 tests in `crates/core/src/tuning.rs:106-232`. The pattern is to use `with_thread_env_lock` (a static `Mutex<()>` to serialise env-var writes) plus `power::with_override` to drive the power probe.
|
||||
|
||||
- `matches_existing_clamp_when_no_clamps_apply` — bare call returns within `[2, 8]`.
|
||||
- `env_var_bypasses_clamps` — `MAGNOTIA_INFERENCE_THREADS=10` returns 10 even with battery + GPU clamps active.
|
||||
- `workload_variants_distinct` — sanity.
|
||||
- `battery_halves_thread_count` — measures both states; battery should be ≤ AC.
|
||||
- `gpu_offload_clamps_llm_to_floor` / `gpu_offload_clamps_whisper_to_floor` — GPU clamp.
|
||||
- `whisper_gpu_floor_is_at_least_llm_gpu_floor` — architectural invariant.
|
||||
- `gpu_offload_off_does_not_clamp_below_battery_calc` — GPU clamp does not undo the battery halve when GPU is off.
|
||||
- `logging_does_not_panic` — smoke for the tracing path.
|
||||
|
||||
## Watch-outs
|
||||
|
||||
- **Two static `Mutex`es: `SEEN` and `TEST_LOCK` (in tests).** Holding either across a panic poisons it. The helper uses `if let Ok(mut seen) = log_seen().lock()` rather than `.expect()` so a poisoned `SEEN` mutex degrades silently to "log every call" rather than panicking.
|
||||
- **`MAGNOTIA_INFERENCE_THREADS` is a process-global env-var.** Per-call overrides need to wrap the call site in a temporary env-var, which is racy across threads. Tests serialise via `THREAD_ENV_LOCK`.
|
||||
- **Battery probe is a cross-process state.** A user toggling the dock between calls sees the change after the 10-second power cache TTL. Acceptable for thread tuning; consumers wanting near-real-time should call `force_clear_cache` (test-only) or accept the 10 s lag.
|
||||
- **`probe_gpu()` returning `None` does not affect this helper.** The `gpu_offloaded` flag is passed in by the caller (the LLM engine sets it from `use_gpu && gpu_layers >= n_layer()`). See the slice-4 LLM page for the call site.
|
||||
|
||||
## See also
|
||||
|
||||
- [Power-state probe](core-power.md)
|
||||
- [Constants module](core-constants.md) (RAM thresholds)
|
||||
- [Slice 3 transcription engines](../03-audio-transcription/README.md)
|
||||
- [Slice 4 LLM engine](../04-llm-formatting-mcp/README.md)
|
||||
- `docs/superpowers/specs/2026-05-09-battery-gpu-aware-thread-tuning-design.md` for the full design rationale.
|
||||
@@ -0,0 +1,164 @@
|
||||
---
|
||||
name: Core types and enums
|
||||
type: architecture-map-page
|
||||
slice: 05-core-storage-hotkey-build
|
||||
last_verified: 2026/05/09
|
||||
---
|
||||
|
||||
# Core types and enums
|
||||
|
||||
> **Where you are:** [Architecture map](../README.md) → [Core, Storage, Hotkey, Build](README.md) → Core types and enums
|
||||
|
||||
**Plain English summary.** This is the public type vocabulary every other crate in Magnotia builds on top of. Newtype wrappers (`ModelId`, `EngineName`, `Megabytes`) prevent stringly-typed and unit-mistake bugs at compile time. `Segment` and `Transcript` are the structured shape of a transcription result. Everything is `Serialize` / `Deserialize` so it crosses the Tauri IPC boundary unchanged.
|
||||
|
||||
## At a glance
|
||||
|
||||
- File: `crates/core/src/types.rs` (177 LOC).
|
||||
- Re-exported from `crates/core/src/lib.rs` at the top level: `AudioSamples`, `DownloadProgress`, `EngineName`, `Megabytes`, `ModelId`, `Segment`, `Transcript`, `TranscriptionOptions`.
|
||||
- External deps: `serde 1` only.
|
||||
- Consumers: every workspace crate. The frontend receives these shapes via Tauri-serialised JSON.
|
||||
|
||||
## What's in here
|
||||
|
||||
### `ModelId` — `crates/core/src/types.rs:5`
|
||||
|
||||
Stringly-typed wrapper around a model identifier. Prevents passing raw strings where a model id is expected.
|
||||
|
||||
```rust
|
||||
pub struct ModelId(String);
|
||||
impl ModelId {
|
||||
pub fn new(id: impl Into<String>) -> Self;
|
||||
pub fn as_str(&self) -> &str;
|
||||
}
|
||||
impl Display for ModelId;
|
||||
```
|
||||
|
||||
Used in `MagnotiaError::ModelNotFound`, `ModelEntry.id`, `DownloadProgress.model_id`, the LLM model registry (slice 4), the speech model registry (this crate, see [`core-model-registry.md`](core-model-registry.md)), and the file-system path API (`AppPaths::speech_model_dir`).
|
||||
|
||||
### `EngineName` — `crates/core/src/types.rs:25`
|
||||
|
||||
Same shape as `ModelId`, separate type. Prevents an engine name being passed where a model id is expected and vice versa.
|
||||
|
||||
```rust
|
||||
pub struct EngineName(String);
|
||||
impl EngineName {
|
||||
pub fn new(name: impl Into<String>) -> Self;
|
||||
pub fn as_str(&self) -> &str;
|
||||
}
|
||||
impl Display for EngineName;
|
||||
```
|
||||
|
||||
### `Megabytes` — `crates/core/src/types.rs:45`
|
||||
|
||||
Newtype around `u64` with display formatting that switches between MB and GB at the kilobyte boundary. Prevents bytes / kilobytes / megabytes / gigabytes mix-ups in the model registry and the recommendation scoring path.
|
||||
|
||||
```rust
|
||||
pub struct Megabytes(pub u64);
|
||||
impl Megabytes {
|
||||
pub fn from_gb(gb: f64) -> Self;
|
||||
pub fn as_gb(&self) -> f64;
|
||||
}
|
||||
impl Display for Megabytes; // "650 MB" or "1.5 GB"
|
||||
```
|
||||
|
||||
`Megabytes` is `Copy`, `PartialOrd`, `PartialEq`, so models can be sorted and compared without ceremony. `saturating_sub` is used at the call site (`recommendation::score_model`) to compute headroom without underflow.
|
||||
|
||||
### `AudioSamples` — `crates/core/src/types.rs:69`
|
||||
|
||||
Wraps a `Vec<f32>` with sample-rate and channel metadata. The `mono_16khz` constructor is the canonical Whisper / Parakeet shape; constants come from [`core-constants.md`](core-constants.md).
|
||||
|
||||
```rust
|
||||
pub struct AudioSamples { samples: Vec<f32>, sample_rate: u32, channels: u16 }
|
||||
impl AudioSamples {
|
||||
pub fn new(samples: Vec<f32>, sample_rate: u32, channels: u16) -> Self;
|
||||
pub fn mono_16khz(samples: Vec<f32>) -> Self;
|
||||
pub fn samples(&self) -> &[f32];
|
||||
pub fn into_samples(self) -> Vec<f32>;
|
||||
pub fn sample_rate(&self) -> u32;
|
||||
pub fn channels(&self) -> u16;
|
||||
pub fn duration_secs(&self) -> f64;
|
||||
}
|
||||
```
|
||||
|
||||
Note: `AudioSamples` does **not** derive `Serialize`. It is an internal Rust shape. The wire format for audio is bytes plus a sidecar metadata struct.
|
||||
|
||||
### `Segment` — `crates/core/src/types.rs:117`
|
||||
|
||||
A single timed chunk of transcribed text. Plain data, public fields.
|
||||
|
||||
```rust
|
||||
pub struct Segment {
|
||||
pub start: f64, // seconds
|
||||
pub end: f64, // seconds
|
||||
pub text: String,
|
||||
}
|
||||
```
|
||||
|
||||
### `Transcript` — `crates/core/src/types.rs:125`
|
||||
|
||||
The full transcription result. Encapsulates the segment list and provides text-join via the `text()` accessor. Fields are private; access is via accessors so the joining logic stays here.
|
||||
|
||||
```rust
|
||||
pub struct Transcript { segments: Vec<Segment>, language: String, duration: f64 }
|
||||
impl Transcript {
|
||||
pub fn new(segments: Vec<Segment>, language: String, duration: f64) -> Self;
|
||||
pub fn text(&self) -> String; // Joins segments with " "
|
||||
pub fn segments(&self) -> &[Segment];
|
||||
pub fn language(&self) -> &str;
|
||||
pub fn duration(&self) -> f64;
|
||||
}
|
||||
```
|
||||
|
||||
### `TranscriptionOptions` — `crates/core/src/types.rs:163`
|
||||
|
||||
Options passed to a transcription engine. `Default` is two `None`s.
|
||||
|
||||
```rust
|
||||
pub struct TranscriptionOptions {
|
||||
pub language: Option<String>,
|
||||
pub initial_prompt: Option<String>,
|
||||
}
|
||||
```
|
||||
|
||||
### `DownloadProgress` — `crates/core/src/types.rs:170`
|
||||
|
||||
Progress envelope emitted from the model downloader to the frontend. `percent: u8` is computed at the source (the downloader rounds).
|
||||
|
||||
```rust
|
||||
pub struct DownloadProgress {
|
||||
pub model_id: ModelId,
|
||||
pub file_name: String,
|
||||
pub bytes_downloaded: u64,
|
||||
pub total_bytes: u64,
|
||||
pub percent: u8,
|
||||
}
|
||||
```
|
||||
|
||||
## Engine, SpeedTier, AccuracyTier
|
||||
|
||||
These three live alongside the model registry, not in `types.rs`. They are documented under [`core-model-registry.md`](core-model-registry.md). Listed here for completeness because the slice scope mentions them:
|
||||
|
||||
- `Engine` — `Whisper` / `Parakeet` / `Moonshine` (`model_registry.rs:7`)
|
||||
- `SpeedTier` — `Instant` / `Fast` / `Moderate` / `Slow` (`model_registry.rs:14`)
|
||||
- `AccuracyTier` — `Excellent` / `Great` / `Good` (`model_registry.rs:23`)
|
||||
- `LanguageSupport` — `EnglishOnly` / `Multilingual(u16)` (`model_registry.rs:31`)
|
||||
|
||||
## Data flow / contract
|
||||
|
||||
- All types in `types.rs` derive `Serialize` and `Deserialize` except `AudioSamples` (Rust-only).
|
||||
- The JSON wire shape on the Tauri boundary is what `serde_json` produces from these structs. No bespoke representation.
|
||||
- `Segment` and `Transcript` are the contract between the transcription engines (slice 3) and the AI-formatting pipeline (slice 4).
|
||||
|
||||
## Watch-outs
|
||||
|
||||
- **Newtypes are not transitively unique.** `ModelId::new("whisper-tiny-en")` and `EngineName::new("whisper-tiny-en")` will both compile. The discipline is at the call site.
|
||||
- **`Transcript.text()` joins with `" "`.** No re-punctuation. Smart-paragraph breaks happen later in the AI-formatting pipeline (`SMART_PARAGRAPH_GAP_SECS` from [`core-constants.md`](core-constants.md)).
|
||||
- **`AudioSamples::duration_secs` returns 0.0 if `sample_rate == 0`.** Defensive; callers should never construct a zero-rate sample bag, but the helper does not panic.
|
||||
|
||||
## See also
|
||||
|
||||
- [Constants module](core-constants.md)
|
||||
- [Error type](core-error.md)
|
||||
- [Model registry (Engine, SpeedTier, AccuracyTier)](core-model-registry.md)
|
||||
- [Slice 3 transcription engines](../03-audio-transcription/README.md)
|
||||
- [Slice 4 AI-formatting pipeline](../04-llm-formatting-mcp/README.md)
|
||||
@@ -0,0 +1,47 @@
|
||||
---
|
||||
name: HANDOVER and root README pointer
|
||||
type: architecture-map-page
|
||||
slice: 05-core-storage-hotkey-build
|
||||
last_verified: 2026/05/09
|
||||
---
|
||||
|
||||
# `HANDOVER.md` and root `README.md` pointer
|
||||
|
||||
> **Where you are:** [Architecture map](../README.md) → [Core, Storage, Hotkey, Build](README.md) → Handover and README pointer
|
||||
|
||||
**Plain English summary.** Two human-facing documents at the repo root: `HANDOVER.md` (the live session-handover note Jake reads at the start of each session) and `README.md` (the standing project README). This page does not duplicate either — it just points readers in the right direction.
|
||||
|
||||
## `HANDOVER.md` (repo root)
|
||||
|
||||
- File: `HANDOVER.md` (~11 KB).
|
||||
- Purpose: end-of-session handover. Captures what shipped, what is in flight, what to do next session, and the open release-blockers.
|
||||
- Status as of 2026/04/25: Phase 9 (LLM content tags + bulk export) mostly shipped. One open MAJOR (RB-08, macOS power assertion) gates v0.1 tagging.
|
||||
- **Drift watch:** `HANDOVER.md` records schema head as v14 (the migration that landed in that session). Migration v15 (`idx_transcripts_profile_created`) has since landed on disk; the next handover should reflect head = v15. Tracked in [`README.md`](README.md) of this slice.
|
||||
|
||||
The handover is the source of truth for "what state is this project actually in right now". Read it first when picking up the project after a break. Do not transcribe it into the architecture map.
|
||||
|
||||
## `README.md` (repo root)
|
||||
|
||||
- File: `README.md` (~24 KB).
|
||||
- Purpose: standing project README for a contributor or curious reader. Covers what Magnotia is, why it exists, quick-start instructions, the `magnotia-` crate map, the rebrand history (Magnotia / Lumenote), and pointers into the documentation.
|
||||
- Audience: external. The architecture map is internal.
|
||||
|
||||
The architecture map (this slice) is the implementation-level reference; the README is the introduction.
|
||||
|
||||
## Other reference docs in `docs/`
|
||||
|
||||
- `docs/audit/phase0-cartography.md` — earlier crate-graph cartography.
|
||||
- `docs/audit/phase1-lean-pass.md` — phase-1 lean pass findings.
|
||||
- `docs/audit/phases-1-8-playbook.md` — playbook for the multi-phase audit.
|
||||
- `docs/code-review-2026-04-22.md` — the full audit findings, source of every RB-N reference.
|
||||
- `docs/dev-setup.md` — local development setup notes.
|
||||
- `docs/handovers/HANDOVER-2026-04-24.md` — archived handover from 24 April.
|
||||
- `docs/issues/*` — critical-issue write-ups. See [`README.md`](README.md) of this slice for the slice-5-relevant ones.
|
||||
- `docs/roadmap/*` — feature roadmaps.
|
||||
- `docs/superpowers/*` — design specs and audit specs (the most recent specs are the source of truth for current direction).
|
||||
- `docs/whisper-ecosystem/*` — research notes on the Whisper / Parakeet / Moonshine ecosystem.
|
||||
|
||||
## See also
|
||||
|
||||
- [Slice 5 README](README.md)
|
||||
- [Code review 2026-04-22](../../code-review-2026-04-22.md)
|
||||
@@ -0,0 +1,118 @@
|
||||
---
|
||||
name: Dev launcher and scripts
|
||||
type: architecture-map-page
|
||||
slice: 05-core-storage-hotkey-build
|
||||
last_verified: 2026/05/09
|
||||
---
|
||||
|
||||
# Dev launcher and scripts
|
||||
|
||||
> **Where you are:** [Architecture map](../README.md) → [Core, Storage, Hotkey, Build](README.md) → Dev launcher and scripts
|
||||
|
||||
**Plain English summary.** Two ways to start Magnotia in development. `run.sh` is the bespoke launcher that starts Vite first and then Tauri (avoiding a Tauri-spawned-second-Vite mess). The `package.json` scripts are the canonical npm-run interface — the same scripts CI uses for the frontend gate.
|
||||
|
||||
## At a glance
|
||||
|
||||
- Files: `run.sh` (636 bytes, +x), `package.json` (1,211 bytes), `package-lock.json` (109 KB).
|
||||
- External tools: `npm`, `npx`, `curl`, `bash` (run.sh); the Node toolchain (package.json scripts).
|
||||
- Consumers: `run.sh` is invoked by Jake on his Monolith. The `package.json` scripts are invoked by both CI (`npm run build`) and developers (`npm run dev`).
|
||||
|
||||
## `run.sh` — the local launcher
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
# Magnotia dev launcher. Starts Vite first, waits for it to bind port 1420,
|
||||
# then runs Tauri without triggering a second Vite instance.
|
||||
# For performance testing use: npm run tauri build
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
export LIBCLANG_PATH=/usr/lib64/llvm21/lib64
|
||||
|
||||
printf 'Starting Vite dev server...\n' >&2
|
||||
npm run dev:frontend &
|
||||
VITE_PID=$!
|
||||
|
||||
until curl -sf http://localhost:1420 > /dev/null 2>&1; do sleep 0.5; done
|
||||
printf 'Vite ready. Launching Tauri...\n' >&2
|
||||
|
||||
cleanup() {
|
||||
kill "$VITE_PID" 2>/dev/null || true
|
||||
}
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
npx tauri dev --config '{"build":{"beforeDevCommand":""}}'
|
||||
```
|
||||
|
||||
### Why this exists
|
||||
|
||||
The default `npm run tauri dev` invokes `tauri dev`, which honours `beforeDevCommand` from `tauri.conf.json` and spawns its own Vite. If Vite is already running (eg during interactive iteration), you get two instances racing for port 1420. The launcher solves it by starting Vite explicitly and disabling Tauri's spawn via `--config '{"build":{"beforeDevCommand":""}}'`.
|
||||
|
||||
### Steps
|
||||
|
||||
1. `set -euo pipefail` — early exit on any command failure or unset variable.
|
||||
2. `cd "$(dirname "$0")"` — anchor to the repo root.
|
||||
3. **Hard-codes `LIBCLANG_PATH=/usr/lib64/llvm21/lib64`** — Fedora 41 default LLVM 21 path. **Breaks on Debian / Ubuntu / macOS.** Worth softening to `LIBCLANG_PATH=${LIBCLANG_PATH:-/usr/lib64/llvm21/lib64}` so a developer-set value wins.
|
||||
4. Spawn `npm run dev:frontend &`; capture PID in `VITE_PID`.
|
||||
5. Poll `curl -sf http://localhost:1420` every 500 ms until 200 OK.
|
||||
6. Trap `EXIT INT TERM` to kill the Vite process group on script exit.
|
||||
7. `npx tauri dev` with `beforeDevCommand` disabled.
|
||||
|
||||
### `LIBCLANG_PATH` rationale
|
||||
|
||||
Bindgen (pulled by `whisper-rs-sys` and `llama-cpp-sys-2`) needs a `libclang.so` at build time. On Fedora's LLVM 21 packaging, the canonical location is `/usr/lib64/llvm21/lib64`. On Debian-family the path is `/usr/lib/llvm-N/lib`; on macOS Homebrew it's `$(brew --prefix llvm)/lib`. The hard-coded value means a clean `git clone` on a non-Fedora machine fails the first build until the developer sets `LIBCLANG_PATH` themselves.
|
||||
|
||||
## `package.json` — npm scripts and deps
|
||||
|
||||
### Scripts
|
||||
|
||||
```json
|
||||
"scripts": {
|
||||
"dev": "npm run dev:frontend",
|
||||
"dev:frontend": "svelte-kit sync && vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch",
|
||||
"tauri": "tauri"
|
||||
}
|
||||
```
|
||||
|
||||
- `npm run dev` — frontend only (no Tauri). Useful for pure-UI iteration.
|
||||
- `npm run build` — Vite production build. CI gate.
|
||||
- `npm run check` — `svelte-check` against `jsconfig.json`. CI gate.
|
||||
- `npm run tauri` — passthrough. `npm run tauri dev`, `npm run tauri build`, etc.
|
||||
|
||||
### Runtime dependencies
|
||||
|
||||
- `@tauri-apps/api 2.10.1` — JS bridge to Tauri commands.
|
||||
- `@tauri-apps/plugin-autostart 2.5.1` — autostart on boot.
|
||||
- `@tauri-apps/plugin-dialog 2.7.1` — native file dialogs.
|
||||
- `@tauri-apps/plugin-global-shortcut 2.3.1` — non-Linux hotkey backend (Linux uses our own `magnotia-hotkey` crate, slice 5).
|
||||
- `@tauri-apps/plugin-notification 2.3.3` — toast notifications.
|
||||
- `@tauri-apps/plugin-opener 2.x` — open external URLs.
|
||||
- `@chenglou/pretext 0.0.5` — stylable text preview.
|
||||
- `lucide-svelte 0.577.0` — icon set.
|
||||
- `svelte-i18n 4.0.1` — i18n.
|
||||
|
||||
### Dev dependencies
|
||||
|
||||
- `@sveltejs/kit 2.58.0` + `@sveltejs/adapter-static 3.0.10` + `@sveltejs/vite-plugin-svelte 5.0.0`.
|
||||
- `@tailwindcss/vite 4.2.1` + `tailwindcss 4.2.1` (Tailwind v4, Vite-plugin-driven, no PostCSS config).
|
||||
- `@tauri-apps/cli 2.x`.
|
||||
- `svelte 5.0.0`, `svelte-check 4.0.0`, `typescript 5.6.2`, `vite 6.4.2`.
|
||||
|
||||
## Watch-outs
|
||||
|
||||
- **`run.sh` `LIBCLANG_PATH` is Fedora-specific.** A new developer on Ubuntu / macOS hits a build failure on first run and has to either edit `run.sh` or pre-set the env var. Worth the conditional default.
|
||||
- **No `dev:fullstack` script.** The Tauri dev path goes through `npx tauri dev` (or `run.sh`), not through an npm script. A `npm run dev:fullstack` shortcut would make discovery easier.
|
||||
- **Tailwind v4 is in flight.** Major version, no PostCSS config. Cross-link to slice 1 for the consumed surface.
|
||||
- **`@tauri-apps/api` is pinned to `2.10.1` (no caret).** Intentional because Tauri's JS bridge can shift behaviour subtly between minors. Pinned ensures we test what we ship.
|
||||
- **Plugin versions are caret-ranged.** Compatible-change updates land transparently. `npm audit` (CI) catches advisories.
|
||||
|
||||
## See also
|
||||
|
||||
- [Workspace Cargo.toml](workspace-cargo.md)
|
||||
- [Frontend build config (vite.config, svelte.config, jsconfig)](frontend-build-config.md)
|
||||
- [CI pipeline](ci-pipeline.md) — invokes `npm run build` and `svelte-check`.
|
||||
- [Slice 1 frontend](../01-frontend/README.md) — consumer of every `@tauri-apps/*` runtime dep.
|
||||
@@ -0,0 +1,124 @@
|
||||
---
|
||||
name: Frontend build config
|
||||
type: architecture-map-page
|
||||
slice: 05-core-storage-hotkey-build
|
||||
last_verified: 2026/05/09
|
||||
---
|
||||
|
||||
# Frontend build config
|
||||
|
||||
> **Where you are:** [Architecture map](../README.md) → [Core, Storage, Hotkey, Build](README.md) → Frontend build config
|
||||
|
||||
**Plain English summary.** Three small config files that pin how Vite, SvelteKit, and TypeScript-flavoured-jsconfig behave. The settings are deliberately minimal — the heavy lifting (routes, components, stores) lives in slice 1.
|
||||
|
||||
## At a glance
|
||||
|
||||
- Files: `vite.config.js` (612 bytes), `svelte.config.js` (214 bytes), `jsconfig.json` (366 bytes).
|
||||
- External: Vite, SvelteKit, Tailwind v4 plugin.
|
||||
- Consumers: every dev / build invocation.
|
||||
|
||||
## `vite.config.js`
|
||||
|
||||
```js
|
||||
import { defineConfig } from "vite";
|
||||
import { sveltekit } from "@sveltejs/kit/vite";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
|
||||
const host = process.env.TAURI_DEV_HOST;
|
||||
|
||||
export default defineConfig(async () => ({
|
||||
plugins: [sveltekit(), tailwindcss()],
|
||||
clearScreen: false,
|
||||
server: {
|
||||
port: 1420,
|
||||
strictPort: true,
|
||||
host: host || false,
|
||||
hmr: host
|
||||
? { protocol: "ws", host, port: 1421 }
|
||||
: undefined,
|
||||
watch: {
|
||||
ignored: ["**/src-tauri/**"],
|
||||
},
|
||||
},
|
||||
}));
|
||||
```
|
||||
|
||||
### Notable settings
|
||||
|
||||
- **`port: 1420 + strictPort: true`.** Tauri's `beforeDevCommand` and the `run.sh` poll both target port 1420. `strictPort` means Vite errors out instead of falling back to 1421+, so a stuck process is loud.
|
||||
- **`clearScreen: false`.** Vite's default behaviour clears the terminal. Disabled here so the Tauri logs stay visible alongside Vite's.
|
||||
- **`TAURI_DEV_HOST` env var.** When set, Vite binds to the network interface and runs HMR on port 1421. Used for testing on a real device while developing on the desktop. Unset on local dev.
|
||||
- **`watch: { ignored: ["**/src-tauri/**"] }`.** Vite's file watcher ignores the Tauri directory; otherwise every Cargo build artefact change would trigger an HMR pass.
|
||||
|
||||
### Why Tailwind v4's Vite plugin
|
||||
|
||||
Tailwind 4 ships a `@tailwindcss/vite` plugin that replaces the v3 PostCSS pipeline. The plugin reads CSS-in-JS / `@import "tailwindcss"` directly from the source files. The repo has no `tailwind.config.js` or `postcss.config.cjs` because v4 derives configuration from the CSS itself.
|
||||
|
||||
## `svelte.config.js`
|
||||
|
||||
```js
|
||||
import adapter from "@sveltejs/adapter-static";
|
||||
|
||||
const config = {
|
||||
kit: {
|
||||
adapter: adapter({
|
||||
fallback: "index.html",
|
||||
}),
|
||||
},
|
||||
};
|
||||
export default config;
|
||||
```
|
||||
|
||||
### Notable settings
|
||||
|
||||
- **`adapter-static`** — Magnotia is a Tauri app, not a server-rendered web app. Static adapter outputs a fully pre-rendered HTML/JS bundle that Tauri serves from its embedded webview.
|
||||
- **`fallback: "index.html"`** — every unknown route serves `index.html`, which lets the SvelteKit client router take over. Without this, `/history` typed directly into the URL bar would 404.
|
||||
|
||||
## `jsconfig.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"moduleResolution": "bundler"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Notable settings
|
||||
|
||||
- **`extends ".svelte-kit/tsconfig.json"`** — SvelteKit generates a tsconfig at `npm run dev:frontend`'s `svelte-kit sync` step. We extend it to add our own strictness flags.
|
||||
- **`allowJs: true + checkJs: true`** — the codebase is JavaScript with TypeScript-aware type checking via JSDoc. svelte-check enforces this.
|
||||
- **`strict: true`** — full strict mode. Null safety, no implicit any, etc. svelte-check is the gate (CI runs `npm run check`).
|
||||
- **`moduleResolution: "bundler"`** — TypeScript 5's bundler-aware resolution. Matches Vite's behaviour (no fake CommonJS round-trip).
|
||||
|
||||
## `static/`
|
||||
|
||||
The static folder maps 1:1 to the served root. Listed here for completeness:
|
||||
|
||||
- `favicon.png` — app icon (web).
|
||||
- `pcm-processor.js` — the audio worklet (cross-link: [`static-assets.md`](static-assets.md)).
|
||||
- `svelte.svg`, `tauri.svg`, `vite.svg` — placeholder logos.
|
||||
- `fonts/` — `atkinson-hyperlegible-next.woff2`, `instrument-serif-italic.woff2`, `jetbrains-mono.woff2`, `lexend-variable.woff2`, `opendyslexic.woff2`.
|
||||
- `textures/` — `grain.png`.
|
||||
|
||||
## Watch-outs
|
||||
|
||||
- **`vite.config.js` is async-returning a config but does no awaiting.** Worth the simplification to a synchronous `defineConfig({ ... })` if no async setup is added.
|
||||
- **`svelte.config.js` does not use `vitePreprocess`.** SvelteKit 2 + Svelte 5 do not require it (Svelte 5's compiler reads JSDoc directly). Keeps the config minimal.
|
||||
- **`jsconfig.json` extends a generated file.** Running `svelte-kit sync` is part of `npm run check` and `npm run dev:frontend`. CI runs it explicitly.
|
||||
- **No `vitest` config.** Frontend unit tests are not part of the current workflow. Coverage is via `svelte-check` (types), e2e dogfooding, and Rust integration tests at the slice-2 boundary.
|
||||
|
||||
## See also
|
||||
|
||||
- [Dev launcher and scripts](dev-launcher-and-scripts.md)
|
||||
- [Static assets](static-assets.md)
|
||||
- [Slice 1 frontend](../01-frontend/README.md) — the routes / components this config builds.
|
||||
@@ -0,0 +1,142 @@
|
||||
---
|
||||
name: Hotkey crate (Linux evdev)
|
||||
type: architecture-map-page
|
||||
slice: 05-core-storage-hotkey-build
|
||||
last_verified: 2026/05/09
|
||||
---
|
||||
|
||||
# Hotkey crate (Linux evdev)
|
||||
|
||||
> **Where you are:** [Architecture map](../README.md) → [Core, Storage, Hotkey, Build](README.md) → Hotkey crate (Linux evdev)
|
||||
|
||||
**Plain English summary.** A global hotkey listener for Linux that reads keypresses straight from `/dev/input/event*` rather than going through a display server. That makes it work on both X11 and Wayland, with no compositor-specific protocol negotiation. macOS and Windows fall back to Tauri's global-shortcut plugin; this crate compiles to a no-op on those platforms.
|
||||
|
||||
## At a glance
|
||||
|
||||
- Crate: `magnotia-hotkey`.
|
||||
- LOC: 632 (`lib.rs` 177, `linux.rs` 426, `stub.rs` 29).
|
||||
- External deps: `tokio 1` (rt + sync + macros + time), `serde 1`, `log 0.4`, plus Linux-only: `evdev 0.12` (with `tokio` feature), `notify 7` (default features off, `macos_fsevent` only), `nix 0.29` (`fs` feature).
|
||||
- Public surface: `HotkeyCombo`, `HotkeyEvent`, `EvdevHotkeyListener`, `check_evdev_access`. Plus the parser `HotkeyCombo::from_tauri_str`.
|
||||
- Consumers: slice 2 (`src-tauri/src/commands/hotkey.rs`) is the only caller in production. The Tauri command holds an `EvdevHotkeyListener` in `tauri::State` and forwards `HotkeyEvent` over `mpsc` to the live-session command.
|
||||
|
||||
## Public surface
|
||||
|
||||
### `HotkeyCombo` — `crates/hotkey/src/lib.rs:33`
|
||||
|
||||
Cross-platform shape. Lives in `lib.rs` so both `linux` and `stub` modules can use it.
|
||||
|
||||
```rust
|
||||
pub struct HotkeyCombo {
|
||||
pub ctrl: bool,
|
||||
pub shift: bool,
|
||||
pub alt: bool,
|
||||
pub super_key: bool,
|
||||
pub key_code: u16, // evdev key code
|
||||
pub label: String, // user-facing, eg "Ctrl+Shift+R"
|
||||
}
|
||||
|
||||
impl HotkeyCombo {
|
||||
pub fn from_tauri_str(s: &str) -> Option<Self>;
|
||||
}
|
||||
```
|
||||
|
||||
### `from_tauri_str` parser — `crates/hotkey/src/lib.rs:50`
|
||||
|
||||
Splits on `+`, accepts `ctrl|control`, `shift`, `alt`, `super|meta|cmd|command`, plus a single trigger key. The key-name → evdev-code mapping at `lib.rs:79` covers A-Z, 0-9, F1-F12, plus arrows, modifiers, and the standard punctuation set. Returns `None` on unmappable input.
|
||||
|
||||
### `HotkeyEvent` — `crates/hotkey/src/linux.rs:23` / `stub.rs:11`
|
||||
|
||||
Defined in both backends:
|
||||
|
||||
```rust
|
||||
pub enum HotkeyEvent { Pressed, Released }
|
||||
```
|
||||
|
||||
`Released` matters for push-to-talk.
|
||||
|
||||
### `EvdevHotkeyListener` — `crates/hotkey/src/linux.rs:32`
|
||||
|
||||
```rust
|
||||
pub struct EvdevHotkeyListener { /* hotkey_tx, shutdown_tx */ }
|
||||
|
||||
impl EvdevHotkeyListener {
|
||||
pub fn start(combo: HotkeyCombo, event_tx: mpsc::Sender<HotkeyEvent>) -> Self;
|
||||
pub fn set_hotkey(&self, combo: HotkeyCombo);
|
||||
pub async fn stop(&self);
|
||||
}
|
||||
```
|
||||
|
||||
`start` spawns:
|
||||
|
||||
1. **One async task per input device** that supports the configured trigger key (initial scan).
|
||||
2. **A watcher task** that listens on `notify` for new files in `/dev/input/` and attaches per-device listeners with a 5-attempt 1-second-backoff loop (udev permissions propagate asynchronously after `inotify CREATE`).
|
||||
|
||||
`set_hotkey` updates a `tokio::sync::watch::Sender<Option<HotkeyCombo>>`. All listener tasks pick up the change; future devices use the new combo.
|
||||
|
||||
`stop` sends `None` on the watch channel and signals the shutdown channel. Per-device tasks exit on the next `hotkey_rx.changed()` poll.
|
||||
|
||||
### `check_evdev_access()` — `crates/hotkey/src/lib.rs:166`
|
||||
|
||||
Probes whether the current user can read evdev devices. On permission denied returns:
|
||||
|
||||
```
|
||||
Permission denied reading /dev/input/eventN. Add your user to the 'input' group:
|
||||
sudo usermod -aG input $USER (then log out and back in)
|
||||
```
|
||||
|
||||
The hotkey command in slice 2 calls this at startup; the message becomes a non-modal toast on first run.
|
||||
|
||||
## Linux backend internals
|
||||
|
||||
### Device hotplug — `crates/hotkey/src/linux.rs:64-136`
|
||||
|
||||
Uses `notify::recommended_watcher` against `/dev/input/`. On inotify `Create(_)` events with a path that starts `event*`, it spawns a 5-attempt retry to attach a per-device listener. Retries are 1-second sleeps; udev permissions propagate over a window of a few hundred milliseconds after device creation.
|
||||
|
||||
Failure modes:
|
||||
|
||||
- **`recommended_watcher` returns `Err`** (rare; minimal containers, BSD pretending to be Linux). Logs and degrades to "no hotplug detection" — the initial scan still picks up devices that exist at startup. Non-fatal.
|
||||
- **`watcher.watch("/dev/input")` returns `Err`** (eg `/dev/input` itself missing). Same fallback.
|
||||
|
||||
### Per-device listener — `crates/hotkey/src/linux.rs:274`
|
||||
|
||||
`device.into_event_stream()` gives an async stream of `evdev::InputEvent`. The listener tracks four modifier flags (`ctrl_held`, `shift_held`, `alt_held`, `super_held`) by watching `KEY_LEFT*` and `KEY_RIGHT*` press / release. When the trigger key matches and modifier state matches, sends `HotkeyEvent::Pressed` or `HotkeyEvent::Released` on `event_tx`.
|
||||
|
||||
### `device_supports_combo` — `crates/hotkey/src/linux.rs:368`
|
||||
|
||||
Filters out devices whose reported `EV_KEY` capability does not include the configured trigger. Replaces the RB-12 hard-coded `KEY_A || KEY_R` filter from the original whisper-overlay port. See [Existing in-repo docs](#existing-in-repo-docs).
|
||||
|
||||
```rust
|
||||
fn device_supports_combo(supported: Option<&AttributeSetRef<Key>>, combo: &HotkeyCombo) -> bool {
|
||||
supported.is_some_and(|keys| keys.contains(Key::new(combo.key_code)))
|
||||
}
|
||||
```
|
||||
|
||||
### Channel close handling — `crates/hotkey/src/linux.rs:328`
|
||||
|
||||
When `event_tx.send(...)` fails, the listener exits cleanly: receiver was dropped, no point spinning. Logged at WARN once per device.
|
||||
|
||||
## Tests
|
||||
|
||||
`crates/hotkey/src/linux.rs:372-426`:
|
||||
|
||||
- `attaches_when_device_supports_configured_trigger` — happy path.
|
||||
- `rejects_when_device_lacks_configured_trigger` — wrong key.
|
||||
- `rejects_when_device_reports_no_keys` — `None` capability set.
|
||||
- `attaches_for_non_a_non_r_trigger` — RB-12 regression: a `Ctrl+Shift+D` binding now attaches to a device that reports KEY_D and rejects a device that only reports KEY_R.
|
||||
|
||||
## Watch-outs
|
||||
|
||||
- **Linux only.** The crate compiles to a no-op (`stub.rs`) on macOS and Windows where Tauri's `plugin-global-shortcut` handles hotkeys natively. Feature parity is maintained across platforms but by two distinct mechanisms — including, for example, hotplug detection (Linux only).
|
||||
- **Requires user in the `input` group.** Documented in the `check_evdev_access` error message. Without group membership, no devices open.
|
||||
- **Modifier tracking is per-device.** A user pressing `Ctrl` on the physical keyboard and the trigger on the laptop's built-in keyboard would not match — modifier state is local to one device's stream. In practice users press all keys on the same keyboard so this is fine, but worth knowing for split keyboard rigs.
|
||||
- **`notify` listens on `/dev/input` non-recursively.** Sub-directories would be missed. None exist today.
|
||||
- **Hotplug retry attempts (5 × 1 s) can stack.** A USB hub that announces 10 keyboards in a burst spawns 10 retry tasks; benign but worth knowing.
|
||||
|
||||
## Existing in-repo docs
|
||||
|
||||
- [`docs/issues/hotkey-linux-device-filter.md`](../../issues/hotkey-linux-device-filter.md) — RB-12. The hard-coded `KEY_A || KEY_R` filter the current `device_supports_combo` replaced.
|
||||
|
||||
## See also
|
||||
|
||||
- [Slice 2 hotkey command](../02-tauri-runtime/README.md) — the only caller.
|
||||
- [Slice 1 preferences (hotkey configuration)](../01-frontend/README.md) — the UI that produces a Tauri-style hotkey string.
|
||||
@@ -0,0 +1,115 @@
|
||||
---
|
||||
name: Static assets and pcm-processor.js worklet
|
||||
type: architecture-map-page
|
||||
slice: 05-core-storage-hotkey-build
|
||||
last_verified: 2026/05/09
|
||||
---
|
||||
|
||||
# Static assets and `pcm-processor.js` worklet
|
||||
|
||||
> **Where you are:** [Architecture map](../README.md) → [Core, Storage, Hotkey, Build](README.md) → Static assets and `pcm-processor.js` worklet
|
||||
|
||||
**Plain English summary.** The `static/` folder ships verbatim with the app. Most files are obvious assets (favicon, fonts, a single texture). The interesting one is `pcm-processor.js`, an `AudioWorkletProcessor` that downsamples microphone audio to the 16 kHz mono PCM that whisper.cpp / Parakeet expect. It runs in the browser's audio worklet thread, not the main JS thread.
|
||||
|
||||
## At a glance
|
||||
|
||||
- Folder: `static/` at the repo root.
|
||||
- Files: `favicon.png`, `svelte.svg`, `tauri.svg`, `vite.svg`, `pcm-processor.js`, `fonts/`, `textures/`.
|
||||
- Total: ~250 KB (mostly the woff2 font files).
|
||||
- Consumers: Vite copies the directory into the build output; the SvelteKit static adapter serves the result; the in-browser webview hosted by Tauri loads from there.
|
||||
|
||||
## `pcm-processor.js` — the audio worklet
|
||||
|
||||
```js
|
||||
class PcmProcessor extends AudioWorkletProcessor {
|
||||
constructor() {
|
||||
super();
|
||||
this.buffer = [];
|
||||
this.ratio = sampleRate / 16000;
|
||||
this.needsResample = Math.abs(this.ratio - 1.0) > 0.01;
|
||||
this.resamplePos = 0;
|
||||
}
|
||||
|
||||
process(inputs) {
|
||||
const input = inputs[0];
|
||||
if (!input || input.length === 0) return true;
|
||||
|
||||
const samples = input[0]; // mono channel
|
||||
if (!samples) return true;
|
||||
|
||||
if (this.needsResample) {
|
||||
// Simple downsampling to 16kHz
|
||||
for (let i = 0; i < samples.length; i++) {
|
||||
this.resamplePos += 1;
|
||||
if (this.resamplePos >= this.ratio) {
|
||||
this.buffer.push(samples[i]);
|
||||
this.resamplePos -= this.ratio;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < samples.length; i++) {
|
||||
this.buffer.push(samples[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Send every ~0.5 seconds at 16 kHz = 8000 samples
|
||||
if (this.buffer.length >= 8000) {
|
||||
this.port.postMessage({ type: "pcm", samples: this.buffer });
|
||||
this.buffer = [];
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
registerProcessor("pcm-processor", PcmProcessor);
|
||||
```
|
||||
|
||||
### What it does
|
||||
|
||||
1. Picks up the device-native sample rate (e.g. 48 kHz) from the `sampleRate` global available inside an `AudioWorkletGlobalScope`.
|
||||
2. Computes a downsampling ratio against 16 kHz. If the device is already 16 kHz (rare), passes samples through.
|
||||
3. The downsampler is **dropwise**: it accumulates a fractional position and emits a sample whenever the position crosses an integer step. No anti-aliasing filter. Acceptable for speech; subtly hurts on music or fast transients but Magnotia is a speech app.
|
||||
4. Buffers up to 8 000 samples (≈ 0.5 s at 16 kHz).
|
||||
5. Posts `{ type: "pcm", samples: [...] }` to the AudioWorklet's port. The main thread bridges this onward to the Rust live-session command.
|
||||
|
||||
### Why this lives in `static/` not `src/`
|
||||
|
||||
Audio worklets must be loaded via `audioContext.audioWorklet.addModule(url)` and resolved as a URL, not as a JS module imported into the bundle. SvelteKit's static adapter serves `static/pcm-processor.js` at `/pcm-processor.js`, which the worklet loader can fetch.
|
||||
|
||||
If we moved this into `src/`, Vite would bundle it and break the worklet load.
|
||||
|
||||
### Cross-link
|
||||
|
||||
The 16 kHz target rate is also the value of `magnotia_core::constants::WHISPER_SAMPLE_RATE`. **The worklet hard-codes the literal `16000` rather than importing the Rust constant** because audio worklets cannot import from the bundle. A coordinated change requires editing both places. See [`core-constants.md`](core-constants.md) for the Rust side.
|
||||
|
||||
The 8 000-sample emit threshold is also the value of `magnotia_core::constants::MIN_CHUNK_SAMPLES`. Same hard-coding issue.
|
||||
|
||||
## Fonts
|
||||
|
||||
`static/fonts/`:
|
||||
|
||||
- `atkinson-hyperlegible-next.woff2` — primary text font; designed for accessibility and dyslexia.
|
||||
- `instrument-serif-italic.woff2` — display italic.
|
||||
- `jetbrains-mono.woff2` — monospace, used in code blocks and the diagnostic-report bundle.
|
||||
- `lexend-variable.woff2` — UI alternative.
|
||||
- `opendyslexic.woff2` — accessibility font, opt-in via preferences.
|
||||
|
||||
Loaded via `@font-face` in `src/app.css` (slice 1). Local-first; no Google Fonts CDN.
|
||||
|
||||
## Textures
|
||||
|
||||
`static/textures/grain.png` — the subtle film-grain overlay used on the home page hero. Slice 1 territory.
|
||||
|
||||
## Watch-outs
|
||||
|
||||
- **`pcm-processor.js` literally hard-codes `16000` and `8000`.** Two magic numbers that must move in lock-step with the Rust constants. If Magnotia ever supports a different speech-model sample rate, this file is one of the touch points. Worth a build-time substitution mechanism (Vite plugin) to inject the constants from a single source.
|
||||
- **The downsampler does no anti-aliasing.** A device sampling at 48 kHz has frequency content above 8 kHz that should be filtered out before decimation. The current dropwise downsampler aliases that content into the 0-8 kHz band. Whisper handles the artefacts well in practice but a one-pole lowpass would be a cheap accuracy gain. Tracked verbally; no doc yet.
|
||||
- **Bypasses Vite optimisation.** Files in `static/` are not minified or fingerprinted. Acceptable for a worklet (load is once per session); cache invalidation is a non-issue because Tauri loads from local disk.
|
||||
- **Browser permission gate.** AudioWorklets require a `MediaStream` and run inside the audio context. Tauri's webview honours `getUserMedia` so the mic permission UI just works on Linux / Windows / macOS.
|
||||
|
||||
## See also
|
||||
|
||||
- [Frontend build config](frontend-build-config.md) — `static/` is served by the static adapter.
|
||||
- [Constants module](core-constants.md) — `WHISPER_SAMPLE_RATE`, `MIN_CHUNK_SAMPLES`.
|
||||
- [Slice 3 audio + transcription](../03-audio-transcription/README.md) — Rust side of the worklet bridge.
|
||||
@@ -0,0 +1,107 @@
|
||||
---
|
||||
name: Storage profiles and profile_terms CRUD
|
||||
type: architecture-map-page
|
||||
slice: 05-core-storage-hotkey-build
|
||||
last_verified: 2026/05/09
|
||||
---
|
||||
|
||||
# Storage profiles and profile_terms CRUD
|
||||
|
||||
> **Where you are:** [Architecture map](../README.md) → [Core, Storage, Hotkey, Build](README.md) → Storage profiles CRUD
|
||||
|
||||
**Plain English summary.** Profiles are user-defined contexts (Work, Personal, Game). Each profile has its own initial prompt and a list of custom dictionary terms. The default profile (`00000000-0000-0000-0000-000000000001`) is seeded by migration v6 and protected by SQL triggers from rename or delete.
|
||||
|
||||
## At a glance
|
||||
|
||||
- Source: `crates/storage/src/database.rs:776-1124`.
|
||||
- Public surface: `list_profiles`, `get_profile`, `create_profile`, `update_profile`, `delete_profile`, `list_profile_terms`, `add_profile_term`, `delete_profile_term`.
|
||||
- Public types: `ProfileRow`, `ProfileTermRow`.
|
||||
- Public constant: `magnotia_storage::DEFAULT_PROFILE_ID = "00000000-0000-0000-0000-000000000001"` at `crates/storage/src/lib.rs:7`.
|
||||
- Consumers: slice 2 profiles command, transcript inserts (every transcript carries a `profile_id`), the LLM prompt builder (slice 4) reads the profile's `initial_prompt` and `profile_terms` to condition cleanup.
|
||||
|
||||
## Public types
|
||||
|
||||
### `ProfileRow` — `crates/storage/src/database.rs:776`
|
||||
|
||||
```rust
|
||||
pub struct ProfileRow {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub initial_prompt: String,
|
||||
pub created_at: String,
|
||||
}
|
||||
```
|
||||
|
||||
### `ProfileTermRow` — `crates/storage/src/database.rs:784`
|
||||
|
||||
```rust
|
||||
pub struct ProfileTermRow {
|
||||
pub id: String,
|
||||
pub profile_id: String,
|
||||
pub term: String,
|
||||
pub note: String,
|
||||
pub created_at: String,
|
||||
}
|
||||
```
|
||||
|
||||
## Functions
|
||||
|
||||
### `list_profiles(pool) -> Result<Vec<ProfileRow>>` — `crates/storage/src/database.rs:950`
|
||||
|
||||
`SELECT ... FROM profiles ORDER BY created_at ASC`. Default profile is always first by virtue of being the earliest-created.
|
||||
|
||||
### `get_profile(pool, id) -> Result<Option<ProfileRow>>` — `crates/storage/src/database.rs:959`
|
||||
|
||||
Single-row select.
|
||||
|
||||
### `create_profile(pool, id, name, initial_prompt) -> Result<ProfileRow>` — `crates/storage/src/database.rs:968`
|
||||
|
||||
UUID is generated by the caller. `name` has a `UNIQUE` constraint at the column level (migration v6); a duplicate name returns a friendly `MagnotiaError::StorageError("Profile name already exists: ...")`.
|
||||
|
||||
### `update_profile(pool, id, name, initial_prompt)` — `crates/storage/src/database.rs:995`
|
||||
|
||||
Renames the profile and / or rewrites the initial prompt. **Updating the default profile's name** raises `MagnotiaError::StorageError` because the `trg_protect_default_profile_rename` trigger (migration v6) calls `RAISE(ABORT, 'cannot rename the default profile')` on any `UPDATE OF id, name` where `OLD.id = DEFAULT_PROFILE_ID`. Updating only `initial_prompt` is allowed.
|
||||
|
||||
### `delete_profile(pool, id) -> Result<()>` — `crates/storage/src/database.rs:1024`
|
||||
|
||||
Two guards before the DELETE:
|
||||
|
||||
1. **Application-layer guard:** if the profile owns any transcripts, refuse with a friendly error pointing the user at re-assignment. Implemented via `transcript_count_for_profile` (`database.rs:1103`).
|
||||
2. **Database-layer guard:** `trg_protect_default_profile_delete` raises `RAISE(ABORT, 'cannot delete the default profile')` when the id matches.
|
||||
|
||||
The cascade on `profile_terms.profile_id` removes the profile's custom terms automatically.
|
||||
|
||||
### `list_profile_terms(pool, profile_id) -> Result<Vec<ProfileTermRow>>` — `crates/storage/src/database.rs:1044`
|
||||
|
||||
`SELECT ... FROM profile_terms WHERE profile_id = ? ORDER BY term ASC`. Used by the LLM prompt builder.
|
||||
|
||||
### `add_profile_term(pool, profile_id, term, note) -> Result<String>` — `crates/storage/src/database.rs:1062`
|
||||
|
||||
Generates a UUID v4 (per `crates/storage/Cargo.toml`'s `features = ["v4"]`), inserts the row, returns the new id. The `Cargo.toml` comment claims "v7 random" but the feature flag is `v4`. Practical effect is identical for our purposes (random unique ids); the comment is the inconsistency to clean up if anyone touches that line.
|
||||
|
||||
### `delete_profile_term(pool, id) -> Result<()>` — `crates/storage/src/database.rs:1085`
|
||||
|
||||
`DELETE FROM profile_terms WHERE id = ?`.
|
||||
|
||||
## Data flow / contract
|
||||
|
||||
- Every transcript references a profile via `transcripts.profile_id` (FK installed in migration v9). `insert_transcript` validates the FK at the application layer for a friendlier error.
|
||||
- The LLM prompt builder loads the active profile's `initial_prompt` and the matching `profile_terms` at cleanup time.
|
||||
- The default profile's id is a `pub const &str` so frontend code via Tauri commands can also reference it as a known constant (it is stamped into the JSON bridge by the Tauri preferences command).
|
||||
|
||||
## Watch-outs
|
||||
|
||||
- **Default profile protection is enforced by triggers, not by code.** A future migration that drops or renames the trigger silently removes the protection. There is no head-schema test that asserts the triggers are still present; today only the v6 migration tests assert protection at DML time. Consider adding a head-schema integration test that explicitly attempts to delete the default profile and asserts the failure.
|
||||
- **Profile name `UNIQUE`-ness is case-sensitive.** "Work" and "work" are two different profiles. Worth a discussion about whether case-insensitive uniqueness is the right behaviour; today it is not enforced.
|
||||
- **`transcript_count_for_profile` is the only application-layer FK guard.** Tasks reference no profile (the table predates the profile concept). If task-to-profile FKs land later, `delete_profile` needs a parallel check.
|
||||
- **No `update_profile_term`.** A user fixing a typo in a term's note has to delete and re-add. Worth adding if the volume of profile terms grows.
|
||||
|
||||
## Tests
|
||||
|
||||
Migration tests cover the v6 trigger protection at `crates/storage/src/migrations.rs:977-1015` (`migration_v6_trigger_rejects_default_profile_delete`, `migration_v6_trigger_rejects_default_profile_rename`). The CRUD functions themselves do not have dedicated unit tests in `database.rs`; coverage is via the slice-2 integration tests.
|
||||
|
||||
## See also
|
||||
|
||||
- [Schema and migrations (v6, v7, v9)](storage-schema-and-migrations.md)
|
||||
- [Storage overview](storage-overview.md)
|
||||
- [Slice 4 LLM prompt builder](../04-llm-formatting-mcp/README.md) — consumer of `list_profile_terms`.
|
||||
@@ -0,0 +1,187 @@
|
||||
---
|
||||
name: Storage settings, error log, feedback, implementation rules
|
||||
type: architecture-map-page
|
||||
slice: 05-core-storage-hotkey-build
|
||||
last_verified: 2026/05/09
|
||||
---
|
||||
|
||||
# Storage settings, error log, feedback, implementation rules
|
||||
|
||||
> **Where you are:** [Architecture map](../README.md) → [Core, Storage, Hotkey, Build](README.md) → Settings and miscellaneous CRUD
|
||||
|
||||
**Plain English summary.** The smaller persistence surfaces. Settings is a key-value store. Error log captures structured errors. Feedback captures HITL thumbs and corrections so the LLM prompt builder can inject few-shot examples. Implementation rules persist the if-then automation rules from Phase 12.
|
||||
|
||||
## Settings (key-value)
|
||||
|
||||
### Functions — `crates/storage/src/database.rs:754`
|
||||
|
||||
```rust
|
||||
pub async fn set_setting(pool: &SqlitePool, key: &str, value: &str) -> Result<()>;
|
||||
pub async fn get_setting(pool: &SqlitePool, key: &str) -> Result<Option<String>>;
|
||||
```
|
||||
|
||||
`set_setting` is `INSERT OR REPLACE INTO settings`, so it is idempotent. `get_setting` returns `Ok(None)` for a missing key (not `Err`).
|
||||
|
||||
### Known keys
|
||||
|
||||
The `settings` table is keyed `TEXT PRIMARY KEY, value TEXT NOT NULL`. The conventions are:
|
||||
|
||||
- **`magnotia_preferences`** — JSON blob. The frontend's full preferences object. Read at boot, written on every preference change. This is the bridge between the frontend's reactive preferences store (slice 1) and persistence (slice 5). See [Slice 1 preferences store](../01-frontend/README.md) and [Slice 2 preferences command](../02-tauri-runtime/README.md).
|
||||
- **`active_profile_id`** — string. The profile id the user is actively transcribing into.
|
||||
- Other keys are added ad-hoc per feature; no enumeration in code today.
|
||||
|
||||
## Error log
|
||||
|
||||
### Schema (migration v1)
|
||||
|
||||
```sql
|
||||
CREATE TABLE error_log (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
timestamp TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
context TEXT NOT NULL,
|
||||
error_code TEXT,
|
||||
message TEXT NOT NULL,
|
||||
metadata TEXT
|
||||
);
|
||||
CREATE INDEX idx_error_log_context ON error_log(context);
|
||||
```
|
||||
|
||||
### Public type — `crates/storage/src/database.rs:1149`
|
||||
|
||||
```rust
|
||||
pub struct ErrorLogRow {
|
||||
pub id: i64,
|
||||
pub timestamp: String,
|
||||
pub context: String,
|
||||
pub error_code: Option<String>,
|
||||
pub message: String,
|
||||
pub metadata: Option<String>,
|
||||
}
|
||||
```
|
||||
|
||||
### Functions
|
||||
|
||||
- **`log_error(pool, context, error_code, message, metadata)`** — `crates/storage/src/database.rs:1126`. Append-only insert. Called from anywhere a meaningful structured error happens (audio capture failure, model download failure, transcription engine error, etc).
|
||||
- **`prune_error_log(pool, keep_days) -> Result<u64>`** — `crates/storage/src/database.rs:1166`. `DELETE FROM error_log WHERE timestamp < datetime('now', '-{keep_days} days')`. Called once on app startup. 90 days is the default (slice 2). Returns the affected row count for logging.
|
||||
- **`list_recent_errors(pool, limit)`** — `crates/storage/src/database.rs:1178`. Used by the diagnostic-report bundler in Settings → About to capture recent errors into the export.
|
||||
|
||||
## Feedback (HITL)
|
||||
|
||||
Phase 2 of the feature-complete roadmap. Captures thumbs + corrections on AI-generated output so the prompt builder can inject recent examples as few-shot exemplars. Storage-only here; the prompt-conditioning logic lives in `magnotia-llm` (slice 4).
|
||||
|
||||
### Schema (migration v10)
|
||||
|
||||
```sql
|
||||
CREATE TABLE feedback (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
target_type TEXT NOT NULL,
|
||||
target_id TEXT,
|
||||
rating INTEGER NOT NULL, -- -1 thumbs down, 0 correction (neutral), +1 thumbs up
|
||||
original_text TEXT,
|
||||
corrected_text TEXT,
|
||||
context_json TEXT,
|
||||
profile_id TEXT NOT NULL DEFAULT '<DEFAULT_PROFILE_ID>',
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
CREATE INDEX idx_feedback_target_type_rating ON feedback(target_type, rating);
|
||||
CREATE INDEX idx_feedback_profile ON feedback(profile_id);
|
||||
```
|
||||
|
||||
### Public types — `crates/storage/src/database.rs:1210, 1241, 1253`
|
||||
|
||||
```rust
|
||||
pub enum FeedbackTargetType { MicroStep, TaskExtraction, Cleanup }
|
||||
impl FeedbackTargetType {
|
||||
pub fn as_str(self) -> &'static str; // "microstep" | "task_extraction" | "cleanup"
|
||||
pub fn parse(s: &str) -> Option<Self>; // (named to avoid the FromStr trait)
|
||||
}
|
||||
|
||||
pub struct RecordFeedbackParams {
|
||||
pub target_type: FeedbackTargetType,
|
||||
pub target_id: Option<String>,
|
||||
pub rating: i8, // -1, 0, or +1
|
||||
pub original_text: Option<String>,
|
||||
pub corrected_text: Option<String>,
|
||||
pub context_json: Option<String>,
|
||||
pub profile_id: Option<String>, // None falls back to DEFAULT_PROFILE_ID
|
||||
}
|
||||
|
||||
pub struct FeedbackRow {
|
||||
pub id: i64,
|
||||
pub target_type: String,
|
||||
pub target_id: Option<String>,
|
||||
pub rating: i64,
|
||||
pub original_text: Option<String>,
|
||||
pub corrected_text: Option<String>,
|
||||
pub context_json: Option<String>,
|
||||
pub profile_id: String,
|
||||
pub created_at: String,
|
||||
}
|
||||
```
|
||||
|
||||
### Functions
|
||||
|
||||
- **`record_feedback(pool, params) -> Result<i64>`** — `crates/storage/src/database.rs:1265`. Validates `rating ∈ -1..=1`; falls `profile_id` back to `DEFAULT_PROFILE_ID`; returns the inserted row id.
|
||||
- **`list_feedback_examples(pool, target_type, limit, min_rating, profile_id)`** — `crates/storage/src/database.rs:1303`. Returns the most recent rows scoped to the active profile, ordered `created_at DESC, id DESC` (recency wins ties). `min_rating` filters out thumbs-down examples when the caller wants positive reinforcement only; pass `-1` to include everything.
|
||||
|
||||
The "scoped to the active profile" behaviour exists so feedback does not cross profiles — corrections you made while transcribing for Work do not leak into Personal.
|
||||
|
||||
## Implementation rules (Phase 12)
|
||||
|
||||
If-then automation rules. Each rule has a trigger kind and value (eg "phrase contains 'remind me'"), a JSON-encoded list of actions, and a `last_fired_key` so a rule does not double-fire on the same trigger.
|
||||
|
||||
### Schema (migration v12)
|
||||
|
||||
```sql
|
||||
CREATE TABLE implementation_rules (
|
||||
id TEXT PRIMARY KEY,
|
||||
enabled INTEGER NOT NULL,
|
||||
trigger_kind TEXT NOT NULL,
|
||||
trigger_value TEXT NOT NULL,
|
||||
actions_json TEXT NOT NULL,
|
||||
last_fired_key TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
CREATE INDEX idx_implementation_rules_enabled_trigger
|
||||
ON implementation_rules(enabled, trigger_kind);
|
||||
```
|
||||
|
||||
### Public type — `crates/storage/src/database.rs:845`
|
||||
|
||||
```rust
|
||||
pub struct ImplementationRuleRow {
|
||||
pub id: String,
|
||||
pub enabled: bool,
|
||||
pub trigger_kind: String,
|
||||
pub trigger_value: String,
|
||||
pub actions_json: String,
|
||||
pub last_fired_key: Option<String>,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
```
|
||||
|
||||
### Functions
|
||||
|
||||
- **`insert_implementation_rule(pool, id, enabled, trigger_kind, trigger_value, actions_json, last_fired_key)`** — `crates/storage/src/database.rs:636`.
|
||||
- **`list_implementation_rules(pool)`** — `crates/storage/src/database.rs:667`.
|
||||
- **`get_implementation_rule(pool, id)`** — `crates/storage/src/database.rs:680`.
|
||||
- **`set_implementation_rule_enabled(pool, id, enabled)`** — `crates/storage/src/database.rs:697`. Bumps `updated_at`.
|
||||
- **`mark_implementation_rule_fired(pool, id, fired_key)`** — `crates/storage/src/database.rs:720`. Sets `last_fired_key = ?` so the dispatcher knows not to fire again on the same trigger.
|
||||
- **`delete_implementation_rule(pool, id)`** — `crates/storage/src/database.rs:743`.
|
||||
|
||||
## Watch-outs
|
||||
|
||||
- **`magnotia_preferences` is a JSON blob in TEXT.** Schema-on-read. A typo in the frontend store can land in the database and silently round-trip through future loads. A periodic schema-validation pass would catch it.
|
||||
- **`error_log` has no severity column.** Every error is created equal. Worth a future migration if the diagnostic-report bundler needs to highlight critical errors.
|
||||
- **`feedback.profile_id` defaults to `DEFAULT_PROFILE_ID` at the column level.** Combined with the `record_feedback` fallback, this means feedback can never end up profile-orphaned. Good.
|
||||
- **`implementation_rules.actions_json` is unschematised TEXT.** The dispatcher logic owns the parsing. A future schema for actions would catch malformed JSON at the storage layer.
|
||||
- **`set_setting` is unbounded.** A user with a huge `magnotia_preferences` blob (which has happened in dogfooding) drags every read of that key. Today the preferences blob is small (KB-scale).
|
||||
|
||||
## See also
|
||||
|
||||
- [Schema and migrations](storage-schema-and-migrations.md) — v1, v10, v12.
|
||||
- [Storage overview](storage-overview.md)
|
||||
- [Slice 4 LLM prompt builder](../04-llm-formatting-mcp/README.md) — caller of `list_feedback_examples`.
|
||||
- [Slice 2 preferences command](../02-tauri-runtime/README.md) — caller of `set_setting("magnotia_preferences", ...)`.
|
||||
@@ -0,0 +1,128 @@
|
||||
---
|
||||
name: Storage tasks and subtasks CRUD
|
||||
type: architecture-map-page
|
||||
slice: 05-core-storage-hotkey-build
|
||||
last_verified: 2026/05/09
|
||||
---
|
||||
|
||||
# Storage tasks and subtasks CRUD
|
||||
|
||||
> **Where you are:** [Architecture map](../README.md) → [Core, Storage, Hotkey, Build](README.md) → Storage tasks CRUD
|
||||
|
||||
**Plain English summary.** Tasks are the things the user wants to do. Subtasks are tasks with a `parent_task_id` set. Both live in the same `tasks` table with a self-referential foreign key. The completion path includes a small bit of business logic: completing the last subtask auto-completes the parent (and stamps `auto_completed = 1` so the daily-completion analytics can exclude cascade-completed parents).
|
||||
|
||||
## At a glance
|
||||
|
||||
- Source: `crates/storage/src/database.rs:301-631`.
|
||||
- Public surface: `insert_task`, `list_tasks`, `get_task_by_id`, `update_task`, `set_task_energy`, `complete_task`, `uncomplete_task`, `delete_task`, `insert_subtask`, `list_subtasks`, `complete_subtask_and_check_parent`, `list_recent_completions`.
|
||||
- Public types: `TaskRow`, `DailyCompletionCount`.
|
||||
- Consumers: slice 2 task commands; the LLM micro-step decomposer (slice 4) calls `insert_subtask`; the gamification badge on the Tasks page reads `list_recent_completions`.
|
||||
|
||||
## Public types
|
||||
|
||||
### `TaskRow` — `crates/storage/src/database.rs:825`
|
||||
|
||||
```rust
|
||||
pub struct TaskRow {
|
||||
pub id: String,
|
||||
pub text: String,
|
||||
pub bucket: String,
|
||||
pub list_id: Option<String>,
|
||||
pub effort: Option<String>,
|
||||
pub notes: String, // v4
|
||||
pub done: bool,
|
||||
pub done_at: Option<String>,
|
||||
pub created_at: String,
|
||||
pub source_transcript_id: Option<String>,
|
||||
pub parent_task_id: Option<String>, // v3
|
||||
pub energy: Option<String>, // v11; "high" | "medium" | "brain_dead" | None
|
||||
}
|
||||
```
|
||||
|
||||
### `DailyCompletionCount` — `crates/storage/src/database.rs:573`
|
||||
|
||||
```rust
|
||||
#[derive(serde::Serialize)]
|
||||
pub struct DailyCompletionCount {
|
||||
pub day: String, // "YYYY-MM-DD" in local time
|
||||
pub count: u32,
|
||||
}
|
||||
```
|
||||
|
||||
Serialised because slice 2 forwards it to the frontend sparkline.
|
||||
|
||||
## Functions
|
||||
|
||||
### `insert_task(pool, id, text, bucket, source_transcript_id, list_id, effort, energy)` — `crates/storage/src/database.rs:301`
|
||||
|
||||
Positional signature, eight arguments. Documented at `database.rs:293` as deliberately flat — it mirrors the `tasks` schema columns. Refactor to a params struct only if another nullable lands after `energy`.
|
||||
|
||||
### `list_tasks(pool) -> Result<Vec<TaskRow>>` — `crates/storage/src/database.rs:328`
|
||||
|
||||
`SELECT ... FROM tasks ORDER BY created_at DESC`. Returns every row, including completed ones. Frontend filters.
|
||||
|
||||
### `get_task_by_id(pool, id) -> Result<Option<TaskRow>>` — `crates/storage/src/database.rs:341`
|
||||
|
||||
Single-row select.
|
||||
|
||||
### `update_task(pool, id, text, bucket, list_id, effort, notes, energy)` — `crates/storage/src/database.rs:361`
|
||||
|
||||
Updates the user-editable fields. `done` and `done_at` are not updated through this function — completion has its own pair (`complete_task` / `uncomplete_task`).
|
||||
|
||||
### `set_task_energy(pool, id, energy) -> Result<TaskRow>` — `crates/storage/src/database.rs:402`
|
||||
|
||||
Single-column update. Returns the row post-update so the frontend gets confirmation. Energy must be one of `"high"`, `"medium"`, `"brain_dead"`, or `None` — enforced by the DB-level `CHECK` from migration v11.
|
||||
|
||||
### `insert_subtask(pool, parent_id, id, text, bucket)` — `crates/storage/src/database.rs:415`
|
||||
|
||||
Same as `insert_task` but with `parent_task_id` set. Inherits the `bucket` from the parent.
|
||||
|
||||
### `list_subtasks(pool, parent_id) -> Result<Vec<TaskRow>>` — `crates/storage/src/database.rs:431`
|
||||
|
||||
`SELECT ... FROM tasks WHERE parent_task_id = ? ORDER BY created_at ASC`.
|
||||
|
||||
### `complete_subtask_and_check_parent(pool, subtask_id) -> Result<()>` — `crates/storage/src/database.rs:447`
|
||||
|
||||
Business logic, in one function so it lives in one transaction:
|
||||
|
||||
1. Mark the subtask done.
|
||||
2. If every sibling subtask is now done, mark the parent done with `auto_completed = 1`.
|
||||
|
||||
The `auto_completed` flag (v13) lets `list_recent_completions` exclude cascade-completed parents from the daily-count sparkline so the user sees one count per real action, not one count per parent action plus one per subtask completion.
|
||||
|
||||
### `complete_task(pool, id)` — `crates/storage/src/database.rs:498`
|
||||
|
||||
`UPDATE tasks SET done = 1, done_at = datetime('now') WHERE id = ?`.
|
||||
|
||||
### `uncomplete_task(pool, id)` — `crates/storage/src/database.rs:507`
|
||||
|
||||
`UPDATE tasks SET done = 0, done_at = NULL, auto_completed = 0 WHERE id = ?`.
|
||||
|
||||
This is the function flagged in the 2026-04-22 review as RB-15 ("doesn't reopen auto-completed parents"). The current implementation only un-completes the row passed in. If a user un-completes a child of an auto-completed parent, the parent stays done. The fix is to walk up the `parent_task_id` chain and reset auto-completed parents whose subtasks are no longer all-done. Tracked as a release-blocker (per `code-review-2026-04-22.md`); resolution status to verify against current `HANDOVER.md`.
|
||||
|
||||
### `delete_task(pool, id)` — `crates/storage/src/database.rs:550`
|
||||
|
||||
`DELETE FROM tasks WHERE id = ?`. The `parent_task_id REFERENCES tasks(id) ON DELETE CASCADE` from migration v3 removes child subtasks automatically.
|
||||
|
||||
### `list_recent_completions(pool, days) -> Result<Vec<DailyCompletionCount>>` — `crates/storage/src/database.rs:578`
|
||||
|
||||
The Phase 8 momentum sparkline. Steps:
|
||||
|
||||
1. Clamp `days` to `[1, 365]`.
|
||||
2. Group `tasks` rows by `DATE(done_at, 'localtime')` for `done = 1 AND auto_completed = 0`.
|
||||
3. Compose a fixed-length output array by left-joining the grouped result against an N-day "spine" generated by re-querying SQLite for each offset (so SQLite owns the calendar arithmetic, including DST and month boundaries).
|
||||
|
||||
Today the frontend always asks for `days = 7`. The clamp prevents a wild value producing an empty or huge series.
|
||||
|
||||
## Watch-outs
|
||||
|
||||
- **`uncomplete_task` does not reopen auto-completed parents.** RB-15 in the 2026-04-22 review. Status: see slice debt section in [`README.md`](README.md).
|
||||
- **`list_tasks` returns every task ever created.** Soft-delete is not modelled; deleted tasks are gone. Completed tasks pile up over time. Frontend is responsible for filtering.
|
||||
- **Energy column accepts NULL but the `CHECK` only fires on non-NULL values.** Migration v11's check is `CHECK (energy IN ('high', 'medium', 'brain_dead') OR energy IS NULL)`. Worth a regression test that asserts an invalid string fails.
|
||||
- **`auto_completed` flag is set in code, not via trigger.** A future migration that adds another path to "complete this task" (eg an LLM-driven auto-complete) needs to remember to set the flag. Worth a row-level audit.
|
||||
|
||||
## See also
|
||||
|
||||
- [Schema and migrations](storage-schema-and-migrations.md) — v3, v4, v11, v13 are the relevant migrations.
|
||||
- [Storage overview](storage-overview.md)
|
||||
- [Slice 4 LLM decompose-task](../04-llm-formatting-mcp/README.md) — caller of `insert_subtask`.
|
||||
@@ -0,0 +1,137 @@
|
||||
---
|
||||
name: Storage transcripts CRUD
|
||||
type: architecture-map-page
|
||||
slice: 05-core-storage-hotkey-build
|
||||
last_verified: 2026/05/09
|
||||
---
|
||||
|
||||
# Storage transcripts CRUD
|
||||
|
||||
> **Where you are:** [Architecture map](../README.md) → [Core, Storage, Hotkey, Build](README.md) → Storage transcripts CRUD
|
||||
|
||||
**Plain English summary.** All the read/write operations against the `transcripts` table. Inserts validate the profile FK at the application layer in addition to the database constraint so the error message is friendly. Updates split between content updates (`update_transcript`) and metadata updates (`update_transcript_meta`).
|
||||
|
||||
## At a glance
|
||||
|
||||
- Source: `crates/storage/src/database.rs:80-289`.
|
||||
- Public surface: `insert_transcript`, `get_transcript`, `list_transcripts`, `list_transcripts_paged`, `count_transcripts`, `update_transcript`, `update_transcript_meta`, `delete_transcript`, `search_transcripts`.
|
||||
- Public types: `InsertTranscriptParams<'a>` (input), `TranscriptRow` (output).
|
||||
- Consumers: slice 2 transcription command (the post-inference write), the history command (reads), the export plumbing, the MCP server.
|
||||
|
||||
## Public types
|
||||
|
||||
### `InsertTranscriptParams<'a>` — `crates/storage/src/database.rs:61`
|
||||
|
||||
Borrowed-string parameter struct. 16 fields; one-to-one with the v1+v8+v14 column set:
|
||||
|
||||
```rust
|
||||
pub struct InsertTranscriptParams<'a> {
|
||||
pub id: &'a str,
|
||||
pub text: &'a str,
|
||||
pub source: &'a str,
|
||||
pub profile_id: &'a str,
|
||||
pub title: Option<&'a str>,
|
||||
pub audio_path: Option<&'a str>,
|
||||
pub duration: f64,
|
||||
pub engine: Option<&'a str>,
|
||||
pub model_id: Option<&'a str>,
|
||||
pub inference_ms: Option<i64>,
|
||||
pub sample_rate: Option<i64>,
|
||||
pub audio_channels: Option<i64>,
|
||||
pub format_mode: Option<&'a str>,
|
||||
pub remove_fillers: bool,
|
||||
pub british_english: bool,
|
||||
pub anti_hallucination: bool,
|
||||
}
|
||||
```
|
||||
|
||||
`segments_json`, `manual_tags`, `template`, `language`, `starred`, `llm_tags` are not on the insert struct — they default at the column level (`''` or `0`) and are populated later via `update_transcript_meta`.
|
||||
|
||||
### `TranscriptRow` — `crates/storage/src/database.rs:793`
|
||||
|
||||
```rust
|
||||
pub struct TranscriptRow {
|
||||
pub id: String,
|
||||
pub text: String,
|
||||
pub source: String,
|
||||
pub profile_id: String,
|
||||
pub title: Option<String>,
|
||||
pub audio_path: Option<String>,
|
||||
pub duration: f64,
|
||||
pub engine: Option<String>,
|
||||
pub model_id: Option<String>,
|
||||
pub inference_ms: Option<i64>,
|
||||
pub sample_rate: Option<i64>,
|
||||
pub audio_channels: Option<i64>,
|
||||
pub format_mode: Option<String>,
|
||||
pub remove_fillers: bool,
|
||||
pub british_english: bool,
|
||||
pub anti_hallucination: bool,
|
||||
pub created_at: String,
|
||||
pub starred: bool,
|
||||
pub manual_tags: String,
|
||||
pub template: String,
|
||||
pub language: String,
|
||||
pub segments_json: String,
|
||||
pub llm_tags: String, // v14
|
||||
}
|
||||
```
|
||||
|
||||
## Functions
|
||||
|
||||
### `insert_transcript(pool, ¶ms) -> Result<()>` — `crates/storage/src/database.rs:80`
|
||||
|
||||
1. **Pre-flight FK check.** `profile_exists(pool, params.profile_id)` (private, `database.rs:1094`) runs `SELECT 1 FROM profiles WHERE id = ?`. If the profile is not present, returns a friendly `MagnotiaError::StorageError("Insert transcript failed: unknown profile id '...'")`. Without this, sqlite would raise `FOREIGN KEY constraint failed` which the frontend cannot easily disambiguate.
|
||||
2. Single `INSERT INTO transcripts (...) VALUES (...)` with all 16 fields.
|
||||
|
||||
### `get_transcript(pool, id) -> Result<Option<TranscriptRow>>` — `crates/storage/src/database.rs:117`
|
||||
|
||||
Single-row select. Returns `Ok(None)` for missing id (not `Err`).
|
||||
|
||||
### `list_transcripts(pool, limit) -> Result<Vec<TranscriptRow>>` — `crates/storage/src/database.rs:129`
|
||||
|
||||
Calls `list_transcripts_paged(pool, limit, 0)`.
|
||||
|
||||
### `list_transcripts_paged(pool, limit, offset) -> Result<Vec<TranscriptRow>>` — `crates/storage/src/database.rs:135`
|
||||
|
||||
`SELECT ... FROM transcripts ORDER BY created_at DESC LIMIT ? OFFSET ?`. The History page in slice 1 paginates through this.
|
||||
|
||||
### `count_transcripts(pool) -> Result<i64>` — `crates/storage/src/database.rs:153`
|
||||
|
||||
`SELECT COUNT(*) FROM transcripts`. Used by the History page to render `(N total)` and to decide whether to render the empty state.
|
||||
|
||||
### `update_transcript(pool, id, ...) -> Result<()>` — `crates/storage/src/database.rs:168`
|
||||
|
||||
Updates the **content** fields: `text`, `title`, `language`, `segments_json`. Triggers `transcripts_au` so the FTS index is refreshed.
|
||||
|
||||
### `update_transcript_meta(pool, id, ...) -> Result<()>` — `crates/storage/src/database.rs:221`
|
||||
|
||||
Updates the **metadata** fields: `starred`, `manual_tags`, `template`, plus `llm_tags` (post-v14). Separate from `update_transcript` because the user-driven star / manual-tag actions are common and we do not want to rebuild FTS index for a starring change. The `transcripts_au` trigger does fire either way (it watches `AFTER UPDATE ON transcripts` with no column predicate), so worth verifying that single-column updates do not bloat the FTS table over time.
|
||||
|
||||
### `delete_transcript(pool, id) -> Result<()>` — `crates/storage/src/database.rs:257`
|
||||
|
||||
`DELETE FROM transcripts WHERE id = ?`. The cascade on `segments.transcript_id` removes child rows. The `transcripts_ad` trigger removes the row from the FTS index.
|
||||
|
||||
### `search_transcripts(pool, query, limit) -> Result<Vec<TranscriptRow>>` — `crates/storage/src/database.rs:270`
|
||||
|
||||
Full-text search via FTS5. Detailed under [`storage-fts5-search.md`](storage-fts5-search.md).
|
||||
|
||||
## Data flow / contract
|
||||
|
||||
- All reads return `TranscriptRow` shaped from the same private helper `transcript_row_from(&SqliteRow)` at `database.rs:856`.
|
||||
- All writes propagate to FTS via the schema-level triggers; no application-level FTS handling is needed.
|
||||
- The `profile_id` is required at insert time; there is no path to an orphaned transcript post-v9.
|
||||
|
||||
## Watch-outs
|
||||
|
||||
- **`update_transcript_meta` does not bump `created_at`.** Correct behaviour: starring a transcript shouldn't change its sort position.
|
||||
- **Stamp columns (`engine`, `model_id`, `inference_ms`, `sample_rate`, `audio_channels`, `format_mode`) are written once at insert and never updated.** They are provenance, not state.
|
||||
- **`remove_fillers`, `british_english`, `anti_hallucination` are also stamped once at insert.** They record what setting was active when the transcript was produced, not the user's current preference.
|
||||
- **No bulk insert.** Each transcript is one round-trip. A future bulk-import path would batch `BEGIN; INSERT...x; COMMIT` for throughput.
|
||||
|
||||
## See also
|
||||
|
||||
- [Schema and migrations](storage-schema-and-migrations.md)
|
||||
- [FTS5 search](storage-fts5-search.md)
|
||||
- [Storage overview](storage-overview.md)
|
||||
- [Slice 2 transcription command](../02-tauri-runtime/README.md) — the immediate caller of `insert_transcript`.
|
||||
@@ -0,0 +1,60 @@
|
||||
---
|
||||
name: Storage file paths
|
||||
type: architecture-map-page
|
||||
slice: 05-core-storage-hotkey-build
|
||||
last_verified: 2026/05/09
|
||||
---
|
||||
|
||||
# Storage file paths
|
||||
|
||||
> **Where you are:** [Architecture map](../README.md) → [Core, Storage, Hotkey, Build](README.md) → Storage file paths
|
||||
|
||||
**Plain English summary.** Thin re-export layer. `magnotia-storage::file_storage` proxies the `magnotia-core::paths::AppPaths` accessors so callers that already depend on `magnotia-storage` do not need to add an explicit `magnotia-core` import to reach the database path.
|
||||
|
||||
## At a glance
|
||||
|
||||
- File: `crates/storage/src/file_storage.rs` (28 LOC).
|
||||
- External deps: standard library only. Forwards into `magnotia_core::paths`.
|
||||
- Public surface: `app_data_dir`, `database_path`, `recordings_dir`, `crashes_dir`, `logs_dir`. Re-exported from `crates/storage/src/lib.rs:23`.
|
||||
- Consumers: slice 2 startup; the diagnostic-report bundler; the live-session recorder writing audio to `recordings_dir`.
|
||||
|
||||
## What's in here
|
||||
|
||||
Every function is a one-liner forwarding into the core path API:
|
||||
|
||||
```rust
|
||||
pub fn app_data_dir() -> PathBuf {
|
||||
magnotia_core::paths::app_paths().app_data_dir()
|
||||
}
|
||||
pub fn database_path() -> PathBuf {
|
||||
magnotia_core::paths::app_paths().database_path()
|
||||
}
|
||||
pub fn recordings_dir() -> PathBuf {
|
||||
magnotia_core::paths::app_paths().recordings_dir()
|
||||
}
|
||||
pub fn crashes_dir() -> PathBuf {
|
||||
magnotia_core::paths::app_paths().crashes_dir()
|
||||
}
|
||||
pub fn logs_dir() -> PathBuf {
|
||||
magnotia_core::paths::app_paths().logs_dir()
|
||||
}
|
||||
```
|
||||
|
||||
## Notes per directory
|
||||
|
||||
- **`database_path`** — `<app_data>/magnotia.db`. Where `magnotia_storage::init` creates / opens the SQLite database. Absent on first run; created with `create_if_missing(true)`.
|
||||
- **`recordings_dir`** — `<app_data>/recordings/`. Audio captures live here. The transcription command (slice 2) writes WAV files; their relative path is stamped onto `transcripts.audio_path`.
|
||||
- **`crashes_dir`** — `<app_data>/crashes/`. The Rust panic hook writes `<unix-ts>-<short-id>.crash` files here. Read by the diagnostic-report bundler in Settings → About.
|
||||
- **`logs_dir`** — `<app_data>/logs/`. Rolling log file (`magnotia.log`, rotated `magnotia.log.1`, etc). The `tracing-subscriber` set up in slice 2 startup (`src-tauri/src/lib.rs`) writes here.
|
||||
|
||||
The model directories (`models_dir`, `speech_model_dir`, `llm_models_dir`) are **not re-exported here**. Callers reach them directly via `magnotia_core::paths::AppPaths` — those calls live in slices 3 (speech models) and 4 (LLM models).
|
||||
|
||||
## Watch-outs
|
||||
|
||||
- **No directory-creation side effects.** These functions return paths only. Callers that need the directory to exist are responsible for `std::fs::create_dir_all`. The storage `init` function does this for the database parent (`crates/storage/src/database.rs:10`); other consumers should follow.
|
||||
- **Repeated calls re-resolve env vars.** Each invocation rebuilds `AppPaths` from scratch. Cheap, but noisy. Slice 2 caches a `OnceLock<AppPaths>` for the app lifetime.
|
||||
|
||||
## See also
|
||||
|
||||
- [Core app paths](core-paths.md) — the underlying `AppPaths` type.
|
||||
- [Storage overview](storage-overview.md)
|
||||
@@ -0,0 +1,85 @@
|
||||
---
|
||||
name: Storage FTS5 transcript search
|
||||
type: architecture-map-page
|
||||
slice: 05-core-storage-hotkey-build
|
||||
last_verified: 2026/05/09
|
||||
---
|
||||
|
||||
# Storage FTS5 transcript search
|
||||
|
||||
> **Where you are:** [Architecture map](../README.md) → [Core, Storage, Hotkey, Build](README.md) → Storage FTS5 transcript search
|
||||
|
||||
**Plain English summary.** Full-text search over transcripts. SQLite's built-in FTS5 module gives us tokenisation, stemming, and BM25 ranking for free. The `transcripts_fts` virtual table is kept in lock-step with `transcripts` by three triggers; `search_transcripts` is the only public read path.
|
||||
|
||||
## At a glance
|
||||
|
||||
- Function: `search_transcripts(pool, query, limit) -> Result<Vec<TranscriptRow>>` at `crates/storage/src/database.rs:270`.
|
||||
- Schema: virtual table + three triggers, all created by migration v2 and rebuilt in migration v9 (table rebuild).
|
||||
- Indexed columns: `text`, `title`. Other columns (engine, model, language) are not searchable today.
|
||||
- Tokenizer: `porter unicode61 remove_diacritics 2` — Porter stemmer, Unicode-normalised, diacritics stripped.
|
||||
- Consumers: slice 2 history command (the search box on the History page).
|
||||
|
||||
## Schema
|
||||
|
||||
Created in migration v2, rebuilt in migration v9 against the new (FK-bearing) `transcripts` table:
|
||||
|
||||
```sql
|
||||
CREATE VIRTUAL TABLE transcripts_fts USING fts5(
|
||||
text, title,
|
||||
content='transcripts',
|
||||
content_rowid='rowid',
|
||||
tokenize='porter unicode61 remove_diacritics 2'
|
||||
);
|
||||
|
||||
CREATE TRIGGER transcripts_ai AFTER INSERT ON transcripts BEGIN
|
||||
INSERT INTO transcripts_fts(rowid, text, title)
|
||||
VALUES (new.rowid, new.text, COALESCE(new.title, ''));
|
||||
END;
|
||||
|
||||
CREATE TRIGGER transcripts_ad AFTER DELETE ON transcripts BEGIN
|
||||
INSERT INTO transcripts_fts(transcripts_fts, rowid, text, title)
|
||||
VALUES ('delete', old.rowid, old.text, COALESCE(old.title, ''));
|
||||
END;
|
||||
|
||||
CREATE TRIGGER transcripts_au AFTER UPDATE ON transcripts BEGIN
|
||||
INSERT INTO transcripts_fts(transcripts_fts, rowid, text, title)
|
||||
VALUES ('delete', old.rowid, old.text, COALESCE(old.title, ''));
|
||||
INSERT INTO transcripts_fts(rowid, text, title)
|
||||
VALUES (new.rowid, new.text, COALESCE(new.title, ''));
|
||||
END;
|
||||
```
|
||||
|
||||
`content='transcripts'` makes `transcripts_fts` a "contentless" external-content index — the FTS table stores tokens but not the data itself, which lives in `transcripts`. This halves on-disk size compared with a self-contained FTS5 table.
|
||||
|
||||
## Query shape — `crates/storage/src/database.rs:275`
|
||||
|
||||
```sql
|
||||
SELECT t.<all columns>
|
||||
FROM transcripts t
|
||||
JOIN transcripts_fts fts ON fts.rowid = t.rowid
|
||||
WHERE transcripts_fts MATCH ?
|
||||
ORDER BY fts.rank
|
||||
LIMIT ?
|
||||
```
|
||||
|
||||
`fts.rank` is FTS5's negative BM25 score (most-relevant first). `MATCH` accepts standard FTS5 syntax: bare words AND together; `OR`, `NOT`, parentheses; quoted multi-word phrases; column-scoped queries (`text:foo title:bar`).
|
||||
|
||||
## Watch-outs
|
||||
|
||||
- **`MATCH` requires the leading word to be a real query, not a malformed token.** A user typing a single quote into the search box will produce a `SQL: malformed match` error from sqlite. The frontend should sanitise or fall back; today it does not, so the error propagates to the toast.
|
||||
- **The triggers fire on every transcript insert / delete / update.** A bulk import of 1,000 transcripts walks the FTS path 1,000 times. Acceptable today (transcripts are user-authored, not imported). If bulk-import lands, consider a `transcripts_fts(transcripts_fts) VALUES('rebuild')` after the bulk insert.
|
||||
- **Rebuild on schema change is expensive.** Migration v9 rebuilt the FTS table and re-tokenised every existing transcript. On a heavy user's database this can take several seconds. v9 ran inside a transaction so a failed rebuild rolls back cleanly.
|
||||
- **`title` is `COALESCE`-d to `''`.** A null title becomes an empty FTS document, not a missing column.
|
||||
- **No FTS over `manual_tags` or `llm_tags`.** Those columns hold comma-joined values and are filtered with `LIKE` queries today. A future migration could add them to the FTS5 index.
|
||||
|
||||
## Tests
|
||||
|
||||
No dedicated FTS test in `database.rs`. Coverage is via the migration tests (which assert the table and triggers exist post-migration) and via the integration test pattern `tx.execute("INSERT INTO transcripts ..."); search_transcripts("phrase")` used in the slice-2 command tests.
|
||||
|
||||
Worth adding a unit test that asserts BM25 ranking actually picks the better match — today a regression in the `tokenize=` clause would not break compile or migration tests.
|
||||
|
||||
## See also
|
||||
|
||||
- [Storage overview](storage-overview.md)
|
||||
- [Schema and migrations](storage-schema-and-migrations.md) — v2 and v9 are the relevant points.
|
||||
- [Transcripts CRUD](storage-crud-transcripts.md)
|
||||
@@ -0,0 +1,98 @@
|
||||
---
|
||||
name: Storage overview
|
||||
type: architecture-map-page
|
||||
slice: 05-core-storage-hotkey-build
|
||||
last_verified: 2026/05/09
|
||||
---
|
||||
|
||||
# Storage overview
|
||||
|
||||
> **Where you are:** [Architecture map](../README.md) → [Core, Storage, Hotkey, Build](README.md) → Storage overview
|
||||
|
||||
**Plain English summary.** The storage crate owns Magnotia's SQLite database, its FTS5 transcript index, the file-system paths it writes to, and the migration machinery that brings a fresh DB up to head. It uses `sqlx 0.8` in a slimmed-down configuration that strips out features the crate does not need.
|
||||
|
||||
## At a glance
|
||||
|
||||
- Crate: `magnotia-storage`.
|
||||
- LOC: 3,771 (database 2,534, migrations 1,185, file_storage 28, lib 24).
|
||||
- External deps: `sqlx 0.8` (`runtime-tokio`, `sqlite`; **no default features**), `tokio 1`, `serde 1`, `log 0.4`, `uuid 1` (v4), `magnotia-core` (path).
|
||||
- Public surface: 46 `pub async fn` (every CRUD verb listed in `crates/storage/src/lib.rs`), one `pub fn` (`as_str`), 9 `pub struct`s (param types + row types), 1 `pub enum` (`FeedbackTargetType`), 1 `pub const` (`DEFAULT_PROFILE_ID`), plus the file-storage path re-exports.
|
||||
- Consumers: every Tauri command that persists or reads (slice 2); the MCP server (slice 4) opens the same database read-only via `init_readonly`.
|
||||
|
||||
## sqlx configuration
|
||||
|
||||
```toml
|
||||
sqlx = { version = "0.8", default-features = false, features = ["runtime-tokio", "sqlite"] }
|
||||
```
|
||||
|
||||
`default-features = false` strips sqlx's `any`, `macros`, `migrate`, and `json` features. None of these are used:
|
||||
|
||||
- `any` — query builder for "any database backend"; we are SQLite-only.
|
||||
- `macros` — compile-time-checked queries; we use runtime queries via `sqlx::query()` and `sqlx::query_scalar()`.
|
||||
- `migrate` — sqlx's bundled migration runner; we run our own custom migration machinery (see [`storage-schema-and-migrations.md`](storage-schema-and-migrations.md)).
|
||||
- `json` — JSON column adapters; we serialise JSON into TEXT columns via `serde_json::to_string` at the call site.
|
||||
|
||||
This cuts ~40% of sqlx's compile graph, most visibly on Windows MSVC where every proc-macro crate (which `macros` pulls in) becomes a slow .dll link. Documented inline at `crates/storage/Cargo.toml`.
|
||||
|
||||
## Init flow
|
||||
|
||||
### `init(db_path)` — `crates/storage/src/database.rs:9`
|
||||
|
||||
The standard read-write entry point. Steps:
|
||||
|
||||
1. `std::fs::create_dir_all(parent)` so a fresh install has the parent directory.
|
||||
2. `SqliteConnectOptions::new().filename(db_path).create_if_missing(true)`.
|
||||
3. `SqlitePoolOptions::new().max_connections(5).connect_with(options)`.
|
||||
4. `PRAGMA foreign_keys = ON` — without this, sqlite ignores `REFERENCES` clauses at runtime even though they parse.
|
||||
5. `run_migrations(&pool)` — applies any pending migrations. See [`storage-schema-and-migrations.md`](storage-schema-and-migrations.md).
|
||||
|
||||
Returns a `SqlitePool` that is held in `tauri::State` for the rest of the process.
|
||||
|
||||
### `init_readonly(db_path)` — `crates/storage/src/database.rs:40`
|
||||
|
||||
The MCP server's entry point. Same options minus `create_if_missing(true)` and plus `read_only(true)`. `max_connections = 2` because the MCP server is single-purpose.
|
||||
|
||||
The structural read-only constraint matters: it makes "the MCP server cannot mutate user data" a property of the connection rather than a property of the dispatcher being well-behaved. Even if a future MCP tool tries to `INSERT`, sqlite refuses at the connection level.
|
||||
|
||||
## Public surface (lib.rs re-exports)
|
||||
|
||||
The complete public CRUD surface is re-exported from `crates/storage/src/lib.rs`:
|
||||
|
||||
- **Transcripts:** `insert_transcript`, `get_transcript`, `list_transcripts`, `list_transcripts_paged`, `count_transcripts`, `update_transcript`, `update_transcript_meta`, `delete_transcript`, `search_transcripts`.
|
||||
- **Tasks:** `insert_task`, `list_tasks`, `get_task_by_id`, `update_task`, `set_task_energy`, `complete_task`, `uncomplete_task`, `delete_task`. Plus subtasks: `insert_subtask`, `list_subtasks`, `complete_subtask_and_check_parent`. Plus completion analytics: `list_recent_completions`.
|
||||
- **Profiles:** `list_profiles`, `get_profile`, `create_profile`, `update_profile`, `delete_profile`. Plus profile terms: `list_profile_terms`, `add_profile_term`, `delete_profile_term`.
|
||||
- **Settings:** `set_setting`, `get_setting`.
|
||||
- **Error log:** `log_error`, `prune_error_log`, `list_recent_errors`.
|
||||
- **Feedback (HITL):** `record_feedback`, `list_feedback_examples`.
|
||||
- **Implementation rules:** `insert_implementation_rule`, `list_implementation_rules`, `get_implementation_rule`, `set_implementation_rule_enabled`, `mark_implementation_rule_fired`, `delete_implementation_rule`.
|
||||
- **Init:** `init`, `init_readonly`.
|
||||
- **File paths (re-exported from `file_storage.rs`):** `app_data_dir`, `database_path`, `recordings_dir`, `crashes_dir`, `logs_dir`.
|
||||
- **Constants:** `DEFAULT_PROFILE_ID = "00000000-0000-0000-0000-000000000001"`.
|
||||
|
||||
Per-table CRUD is split across the per-page docs in this slice. See:
|
||||
|
||||
- [Transcripts CRUD](storage-crud-transcripts.md)
|
||||
- [Tasks CRUD](storage-crud-tasks.md)
|
||||
- [Profiles CRUD](storage-crud-profiles.md)
|
||||
- [Settings, error log, feedback, implementation rules](storage-crud-settings-and-misc.md)
|
||||
- [FTS5 search](storage-fts5-search.md)
|
||||
- [Schema and migrations](storage-schema-and-migrations.md)
|
||||
- [File paths](storage-file-paths.md)
|
||||
|
||||
## Watch-outs
|
||||
|
||||
- **`PRAGMA foreign_keys = ON` is per-connection, not per-database.** The pool's `max_connections = 5` means we run the pragma once at init on the first connection. SQLite re-applies the pragma on each new pool connection because we set it via the connect options... but actually we don't, we set it after `connect_with`. **This is a latent issue worth verifying:** if a second pool connection opens later, foreign keys may not be enforced on it. Audit candidate.
|
||||
- **No connection-level retry on locked DB.** `SQLITE_BUSY` propagates as `MagnotiaError::StorageError(...)`. With WAL mode + 5 max connections this is rare, but a long-running write under a slow filesystem could trigger it.
|
||||
- **Custom migration runner.** sqlx's bundled `migrate!` macro is not used. The custom runner is documented in [`storage-schema-and-migrations.md`](storage-schema-and-migrations.md) and was the subject of the C3 critical-issue write-up at `docs/issues/c3-migrations-atomicity.md`.
|
||||
|
||||
## Existing in-repo docs
|
||||
|
||||
- [`docs/issues/c3-migrations-atomicity.md`](../../issues/c3-migrations-atomicity.md) — drove the transactional migration design.
|
||||
- [`docs/issues/c4-transcript-profile-fk.md`](../../issues/c4-transcript-profile-fk.md) — drove migration v9.
|
||||
|
||||
## See also
|
||||
|
||||
- [Schema and migrations](storage-schema-and-migrations.md)
|
||||
- [File storage paths](storage-file-paths.md)
|
||||
- [Slice 2 Tauri startup](../02-tauri-runtime/README.md) — the caller of `init`.
|
||||
- [Slice 4 MCP server](../04-llm-formatting-mcp/README.md) — the caller of `init_readonly`.
|
||||
@@ -0,0 +1,278 @@
|
||||
---
|
||||
name: Storage schema and migrations
|
||||
type: architecture-map-page
|
||||
slice: 05-core-storage-hotkey-build
|
||||
last_verified: 2026/05/09
|
||||
---
|
||||
|
||||
# Storage schema and migrations
|
||||
|
||||
> **Where you are:** [Architecture map](../README.md) → [Core, Storage, Hotkey, Build](README.md) → Storage schema and migrations
|
||||
|
||||
**Plain English summary.** The full database schema as currently shipped, plus the append-only history of migrations that brought it to v15. Each migration is one transaction. If a migration fails partway, the transaction rolls back so the schema never ends up half-applied. The schema is forward-only — there are no down-migrations.
|
||||
|
||||
## At a glance
|
||||
|
||||
- File: `crates/storage/src/migrations.rs` (1,185 LOC).
|
||||
- External deps: `sqlx 0.8` (`SqlitePool`), `magnotia_core::error`.
|
||||
- Public surface: `run_migrations(pool: &SqlitePool) -> Result<()>`. That is the only `pub` symbol.
|
||||
- Schema head: **v15**.
|
||||
- Consumers: called by `magnotia_storage::database::init` at `crates/storage/src/database.rs:54`.
|
||||
|
||||
## Migration runner contract
|
||||
|
||||
The migration registry is a `&[(i64, &str, &str)]` triple: `(version, description, sql)`. The runner is at `crates/storage/src/migrations.rs:525`:
|
||||
|
||||
```rust
|
||||
pub async fn run_migrations(pool: &SqlitePool) -> Result<()> {
|
||||
run_migrations_slice(pool, MIGRATIONS).await
|
||||
}
|
||||
```
|
||||
|
||||
`run_migrations_slice` (`migrations.rs:546`):
|
||||
|
||||
1. Ensures `schema_version (version INTEGER PRIMARY KEY, description TEXT NOT NULL, applied_at TEXT NOT NULL DEFAULT datetime('now'))` exists.
|
||||
2. Reads `MAX(version)` from `schema_version`.
|
||||
3. For each `(version, description, sql)` whose `version > current`:
|
||||
- Begin a `tx`.
|
||||
- Split the `sql` blob into individual statements via `split_statements` (a hand-rolled SQLite-trigger-aware splitter at `migrations.rs:483`).
|
||||
- Execute each statement against the transaction.
|
||||
- Insert the version row into `schema_version` inside the same transaction.
|
||||
- Commit.
|
||||
4. Idempotent: re-running on a head schema does nothing.
|
||||
|
||||
Atomicity (one tx per migration) was added in response to the 2026-04-22 review (RB-02 / C3). Documented inline at `migrations.rs:548-565`. **Constraint:** future migrations must avoid SQLite operations that implicitly commit (`VACUUM`, `REINDEX`, `ATTACH`). If one is needed, split it into its own non-transactional migration — reviewer's job to flag.
|
||||
|
||||
## Schema head (v15)
|
||||
|
||||
The shape of every table at head, derived by replaying v1 → v15.
|
||||
|
||||
### `transcripts` — keyed `id TEXT PRIMARY KEY`
|
||||
|
||||
```sql
|
||||
CREATE TABLE transcripts (
|
||||
id TEXT PRIMARY KEY,
|
||||
text TEXT NOT NULL DEFAULT '',
|
||||
source TEXT NOT NULL DEFAULT 'microphone',
|
||||
title TEXT,
|
||||
audio_path TEXT,
|
||||
duration REAL NOT NULL DEFAULT 0.0,
|
||||
engine TEXT,
|
||||
model_id TEXT,
|
||||
inference_ms INTEGER,
|
||||
sample_rate INTEGER,
|
||||
audio_channels INTEGER,
|
||||
format_mode TEXT,
|
||||
remove_fillers INTEGER NOT NULL DEFAULT 0,
|
||||
british_english INTEGER NOT NULL DEFAULT 0,
|
||||
anti_hallucination INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
-- v5 (transcripts_meta):
|
||||
starred INTEGER NOT NULL DEFAULT 0,
|
||||
manual_tags TEXT NOT NULL DEFAULT '',
|
||||
template TEXT NOT NULL DEFAULT '',
|
||||
language TEXT NOT NULL DEFAULT '',
|
||||
segments_json TEXT NOT NULL DEFAULT '',
|
||||
-- v8/v9 (profile FK):
|
||||
profile_id TEXT NOT NULL REFERENCES profiles(id),
|
||||
-- v14:
|
||||
llm_tags TEXT NOT NULL DEFAULT ''
|
||||
);
|
||||
|
||||
CREATE INDEX idx_transcripts_created ON transcripts(created_at);
|
||||
CREATE INDEX idx_transcripts_profile_id ON transcripts(profile_id);
|
||||
CREATE INDEX idx_transcripts_profile_created ON transcripts(profile_id, created_at DESC); -- v15
|
||||
```
|
||||
|
||||
### `segments` — keyed `INTEGER PRIMARY KEY AUTOINCREMENT`
|
||||
|
||||
```sql
|
||||
CREATE TABLE segments (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
transcript_id TEXT NOT NULL REFERENCES transcripts(id) ON DELETE CASCADE,
|
||||
start_time REAL NOT NULL,
|
||||
end_time REAL NOT NULL,
|
||||
text TEXT NOT NULL DEFAULT ''
|
||||
);
|
||||
CREATE INDEX idx_segments_transcript ON segments(transcript_id);
|
||||
```
|
||||
|
||||
(Note: today the engines store full segments in `transcripts.segments_json` rather than this table. The table is kept for backwards compatibility.)
|
||||
|
||||
### `transcripts_fts` — FTS5 virtual table (v2, rebuilt v9)
|
||||
|
||||
```sql
|
||||
CREATE VIRTUAL TABLE transcripts_fts USING fts5(
|
||||
text, title,
|
||||
content='transcripts', content_rowid='rowid',
|
||||
tokenize='porter unicode61 remove_diacritics 2'
|
||||
);
|
||||
```
|
||||
|
||||
Plus three triggers (`transcripts_ai`, `transcripts_ad`, `transcripts_au`) that mirror inserts / deletes / updates from `transcripts` into the FTS index. See [`storage-fts5-search.md`](storage-fts5-search.md).
|
||||
|
||||
### `tasks`
|
||||
|
||||
```sql
|
||||
CREATE TABLE tasks (
|
||||
id TEXT PRIMARY KEY,
|
||||
text TEXT NOT NULL,
|
||||
bucket TEXT NOT NULL DEFAULT 'inbox',
|
||||
list_id TEXT,
|
||||
effort TEXT,
|
||||
done INTEGER NOT NULL DEFAULT 0,
|
||||
done_at TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
source_transcript_id TEXT,
|
||||
-- v3:
|
||||
parent_task_id TEXT REFERENCES tasks(id) ON DELETE CASCADE,
|
||||
-- v4:
|
||||
notes TEXT NOT NULL DEFAULT '',
|
||||
-- v11:
|
||||
energy TEXT, -- CHECK (energy IN ('high', 'medium', 'brain_dead') OR energy IS NULL)
|
||||
-- v13:
|
||||
auto_completed INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE INDEX idx_tasks_bucket ON tasks(bucket);
|
||||
CREATE INDEX idx_tasks_transcript ON tasks(source_transcript_id);
|
||||
CREATE INDEX idx_tasks_parent ON tasks(parent_task_id);
|
||||
CREATE INDEX idx_tasks_energy_created ON tasks(energy, created_at); -- v11
|
||||
CREATE INDEX idx_tasks_done_at_auto_completed ON tasks(done_at, auto_completed); -- v13
|
||||
```
|
||||
|
||||
### `task_lists`
|
||||
|
||||
```sql
|
||||
CREATE TABLE task_lists (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
built_in INTEGER NOT NULL DEFAULT 0,
|
||||
profile_id TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
```
|
||||
|
||||
### `profiles` (v6)
|
||||
|
||||
```sql
|
||||
CREATE TABLE profiles (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
initial_prompt TEXT NOT NULL DEFAULT '',
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
```
|
||||
|
||||
Default profile seeded at id `00000000-0000-0000-0000-000000000001`. Two SQL triggers protect it:
|
||||
|
||||
```sql
|
||||
CREATE TRIGGER trg_protect_default_profile_delete
|
||||
BEFORE DELETE ON profiles
|
||||
WHEN OLD.id = '00000000-0000-0000-0000-000000000001'
|
||||
BEGIN SELECT RAISE(ABORT, 'cannot delete the default profile'); END;
|
||||
|
||||
CREATE TRIGGER trg_protect_default_profile_rename
|
||||
BEFORE UPDATE OF id, name ON profiles
|
||||
WHEN OLD.id = '00000000-0000-0000-0000-000000000001'
|
||||
BEGIN SELECT RAISE(ABORT, 'cannot rename the default profile'); END;
|
||||
```
|
||||
|
||||
### `profile_terms` (v6)
|
||||
|
||||
```sql
|
||||
CREATE TABLE profile_terms (
|
||||
id TEXT PRIMARY KEY,
|
||||
profile_id TEXT NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
|
||||
term TEXT NOT NULL,
|
||||
note TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
CREATE INDEX idx_profile_terms_profile_id ON profile_terms(profile_id);
|
||||
```
|
||||
|
||||
UUIDs for `id` are generated as v4 random per `crates/storage/Cargo.toml`. (The `Cargo.toml` comment says "v7 random" but the feature flag is `["v4"]`. This is a known inconsistency between comment and reality. The code path uses `Uuid::new_v4()`.)
|
||||
|
||||
### `settings`
|
||||
|
||||
Plain key-value store. `key TEXT PRIMARY KEY, value TEXT NOT NULL`. The frontend's preferences blob lives at key `magnotia_preferences`.
|
||||
|
||||
### `error_log`
|
||||
|
||||
Append-only diagnostic log. Pruned by `prune_error_log` on app startup with a 90-day default. See [`storage-crud-settings-and-misc.md`](storage-crud-settings-and-misc.md).
|
||||
|
||||
### `feedback` (v10)
|
||||
|
||||
HITL thumbs + corrections, scoped to profile. See [`storage-crud-settings-and-misc.md`](storage-crud-settings-and-misc.md).
|
||||
|
||||
### `implementation_rules` (v12)
|
||||
|
||||
If-then automation rules. See [`storage-crud-settings-and-misc.md`](storage-crud-settings-and-misc.md).
|
||||
|
||||
### `schema_version`
|
||||
|
||||
Bookkeeping. Created by the migration runner itself.
|
||||
|
||||
## Migration history
|
||||
|
||||
15 migrations, all in `MIGRATIONS: &[(i64, &str, &str)]` at `crates/storage/src/migrations.rs:7`.
|
||||
|
||||
| v | Description | What changes |
|
||||
|---|---|---|
|
||||
| 1 | initial schema | Creates `transcripts`, `segments`, `tasks`, `task_lists`, `settings`, `error_log` plus 5 base indexes. |
|
||||
| 2 | transcripts FTS5 + dictionary table | Creates `transcripts_fts` virtual table, three sync triggers, `dictionary` table (later removed). |
|
||||
| 3 | micro-stepping: `parent_task_id` on tasks | `ALTER tasks ADD COLUMN parent_task_id` (with FK + cascade); `idx_tasks_parent`. |
|
||||
| 4 | tasks_meta: notes column | `ALTER tasks ADD COLUMN notes TEXT NOT NULL DEFAULT ''`. |
|
||||
| 5 | transcripts_meta | Adds `starred`, `manual_tags`, `template`, `language`, `segments_json` columns. Persists what was previously in `localStorage` `magnotia_history`. |
|
||||
| 6 | profiles | Creates `profiles` and `profile_terms` tables; seeds the default profile; copies `dictionary` rows to the default profile's `profile_terms`; installs the two protection triggers. |
|
||||
| 7 | drop_dictionary | Removes the legacy `dictionary` table and its index. Data has already been copied in v6. |
|
||||
| 8 | transcript_profile_provenance | Adds `transcripts.profile_id` (nullable initially); `idx_transcripts_profile_id`. |
|
||||
| 9 | transcript_profile_fk | **Table rebuild.** Reconciles orphaned `transcripts.profile_id` values to the default profile, then rebuilds `transcripts` and `segments` with the FK declared. The FTS table and triggers are dropped and recreated against the rebuilt `transcripts`. Driven by [`docs/issues/c4-transcript-profile-fk.md`](../../issues/c4-transcript-profile-fk.md). |
|
||||
| 10 | feedback: HITL thumbs + correction capture | Creates `feedback` table; two indexes (target_type+rating, profile). |
|
||||
| 11 | tasks: energy tagging | `ALTER tasks ADD COLUMN energy` with `CHECK` constraint; `idx_tasks_energy_created`. |
|
||||
| 12 | implementation intentions | Creates `implementation_rules` table; `idx_implementation_rules_enabled_trigger`. |
|
||||
| 13 | gamification: `auto_completed` | `ALTER tasks ADD COLUMN auto_completed`; `idx_tasks_done_at_auto_completed`. Cascade-completed parents are tagged so the daily-completion query can exclude them. |
|
||||
| 14 | transcripts: `llm_tags` column | `ALTER transcripts ADD COLUMN llm_tags TEXT NOT NULL DEFAULT ''`. Phase 9 LLM content tags. |
|
||||
| 15 | composite (profile_id, created_at DESC) index | `CREATE INDEX idx_transcripts_profile_created`. Speeds up the profile-scoped history page query that currently uses `idx_transcripts_profile_id` alone. |
|
||||
|
||||
## Tests
|
||||
|
||||
Migration tests live alongside the registry at `crates/storage/src/migrations.rs:606-1184`:
|
||||
|
||||
- `test_migrations_run_on_empty_db` — full v1→head replay.
|
||||
- `test_migrations_idempotent` — running twice is a no-op.
|
||||
- `migration_tasks_meta_adds_columns` — v3 ALTER goes through.
|
||||
- `migration_implementation_rules_adds_rule_table` — v12 happy path.
|
||||
- `migration_transcripts_meta_adds_columns` — v5 ALTER goes through.
|
||||
- `migration_transcript_profile_provenance_adds_profile_id` — v8.
|
||||
- `migration_v9_reconciles_orphaned_transcript_profiles_and_adds_fk` — the v9 table rebuild end-to-end.
|
||||
- `test_parent_task_id_cascade_delete` — v3 cascade FK behaviour.
|
||||
- `migration_v6_seeds_default_profile_on_fresh_db`.
|
||||
- `migration_v6_copies_dictionary_rows_to_default_profile_terms`.
|
||||
- `migration_v6_trigger_rejects_default_profile_delete`.
|
||||
- `migration_v6_trigger_rejects_default_profile_rename`.
|
||||
- `migration_v7_drops_dictionary_table`.
|
||||
- `multi_statement_migration_rolls_back_on_failure` — the C3 atomicity guarantee.
|
||||
- `migration_v13_adds_auto_completed_column`.
|
||||
- `migration_v15_creates_profile_created_index` — the head migration.
|
||||
|
||||
## Watch-outs
|
||||
|
||||
- **Append-only.** Modifying an existing migration after release is a contract violation. Add a new one.
|
||||
- **`split_statements` is hand-rolled.** Triggers contain `BEGIN ... END` with embedded semicolons. The splitter at `migrations.rs:483` is trigger-aware (it tracks `BEGIN` / `END` depth) but the parser is hand-rolled rather than a real SQL tokeniser. Adding a new control-flow keyword (eg `CASE WHEN ... END` outside a trigger) could trip it. Test additions are advised.
|
||||
- **Migration v9 is the only table rebuild.** Rebuilds drop the FTS index and recreate it. If a future column needs FK adjustment, the v9 pattern is the template, but it is more risk than an `ALTER`.
|
||||
- **No down migrations.** Recovery from a bad migration is restore-from-backup. The pre-commit pattern is to test on a fresh DB and against a copy of a real one.
|
||||
|
||||
## Existing in-repo docs
|
||||
|
||||
- [`docs/issues/c3-migrations-atomicity.md`](../../issues/c3-migrations-atomicity.md) — RB-02. Drove the per-migration transaction wrapper.
|
||||
- [`docs/issues/c4-transcript-profile-fk.md`](../../issues/c4-transcript-profile-fk.md) — drove migration v9.
|
||||
|
||||
## See also
|
||||
|
||||
- [Storage overview](storage-overview.md)
|
||||
- [FTS5 search](storage-fts5-search.md)
|
||||
- [Transcripts CRUD](storage-crud-transcripts.md)
|
||||
- [Tasks CRUD](storage-crud-tasks.md)
|
||||
- [Profiles CRUD](storage-crud-profiles.md)
|
||||
- [Settings, error log, feedback, implementation rules](storage-crud-settings-and-misc.md)
|
||||
@@ -0,0 +1,82 @@
|
||||
---
|
||||
name: Workspace Cargo.toml
|
||||
type: architecture-map-page
|
||||
slice: 05-core-storage-hotkey-build
|
||||
last_verified: 2026/05/09
|
||||
---
|
||||
|
||||
# Workspace Cargo.toml
|
||||
|
||||
> **Where you are:** [Architecture map](../README.md) → [Core, Storage, Hotkey, Build](README.md) → Workspace Cargo.toml
|
||||
|
||||
**Plain English summary.** The repo-root `Cargo.toml`. Tiny — 161 bytes. Defines the Cargo workspace (one binary at `src-tauri/`, every crate under `crates/*`) and the release profile that produces the Tauri bundle artefacts.
|
||||
|
||||
## At a glance
|
||||
|
||||
- File: `/Cargo.toml` (repo root, 161 bytes).
|
||||
- External: none. The workspace root has no dependencies of its own; member crates pin their own versions.
|
||||
- Consumers: every Cargo invocation. `cargo check --workspace` from the repo root walks the members.
|
||||
|
||||
## Contents
|
||||
|
||||
```toml
|
||||
[workspace]
|
||||
members = ["src-tauri", "crates/*"]
|
||||
resolver = "2"
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
lto = "thin"
|
||||
opt-level = 3
|
||||
panic = "abort"
|
||||
strip = "symbols"
|
||||
```
|
||||
|
||||
## Members
|
||||
|
||||
The `crates/*` glob picks up every directory under `crates/` that contains a `Cargo.toml`. Members at the time of writing:
|
||||
|
||||
- `crates/core/` → `magnotia-core`
|
||||
- `crates/storage/` → `magnotia-storage`
|
||||
- `crates/hotkey/` → `magnotia-hotkey`
|
||||
- `crates/audio/` → audio capture (slice 3)
|
||||
- `crates/transcription/` → transcription engines (slice 3)
|
||||
- `crates/llm/` → `magnotia-llm` (slice 4)
|
||||
- `crates/ai-formatting/` → `magnotia-ai-formatting` (slice 4)
|
||||
- `crates/mcp/` → `magnotia-mcp` (slice 4)
|
||||
- `crates/cloud-providers/` → `magnotia-cloud-providers` (slice 4)
|
||||
- `src-tauri/` → the Tauri binary `magnotia` (slice 2)
|
||||
|
||||
## Resolver
|
||||
|
||||
`resolver = "2"` is mandatory for any workspace that mixes feature flags across `[dependencies]` and `[build-dependencies]` or `[dev-dependencies]`. Without it, features unify in surprising ways (a build-script-only feature can silently affect the runtime crate). Tauri 2's docs require resolver 2.
|
||||
|
||||
## Release profile
|
||||
|
||||
```toml
|
||||
codegen-units = 1 # one codegen unit; longer compile, better optimisation
|
||||
lto = "thin" # cross-crate inlining without full LTO's compile blow-up
|
||||
opt-level = 3 # standard optimised
|
||||
panic = "abort" # smaller binaries; no unwind tables; mandatory for embedded panics
|
||||
strip = "symbols" # debug symbols stripped from release artefacts
|
||||
```
|
||||
|
||||
The `panic = "abort"` setting is a deliberate trade-off:
|
||||
|
||||
- **Plus:** smaller binary, no `__rust_panic_unwind` machinery, faster cold start.
|
||||
- **Minus:** any panic is fatal. The crash hook (slice 2) writes a `.crash` file before the process dies; the diagnostic-report bundler in Settings → About surfaces it on next launch.
|
||||
|
||||
`lto = "thin"` not `"fat"` because thin LTO gives most of the optimisation benefit at a fraction of the compile time. Whisper-rs-sys + llama-cpp-sys-2 are already huge C++ builds; saving build time matters more than the last 5% of optimisation.
|
||||
|
||||
## Watch-outs
|
||||
|
||||
- **No `[workspace.dependencies]` block.** Each member crate pins its own dep versions independently. Side effect: `sqlx 0.8` in `magnotia-storage` and `sysinfo 0.35` in `magnotia-core` could drift between members. A future tidy would centralise common pins.
|
||||
- **No `[patch]` overrides.** Useful to know — every crate is consumed at its registered version.
|
||||
- **`codegen-units = 1` makes release builds slow.** ~10 minutes on Jake's Monolith for a clean `cargo tauri build`. Acceptable for release; use `cargo tauri dev` (debug, default) for iteration.
|
||||
- **No `[profile.dev]` overrides.** Default debug build. `cargo build` produces a binary in `target/debug/`. The workspace target dir is `./target` at the repo root, **not `src-tauri/target`** — this caught the CI cache step (see [`ci-pipeline.md`](ci-pipeline.md)).
|
||||
|
||||
## See also
|
||||
|
||||
- [CI pipeline](ci-pipeline.md) — uses this profile for release builds.
|
||||
- [Dev launcher and scripts](dev-launcher-and-scripts.md) — `npm run tauri dev` uses the default debug profile.
|
||||
- [Storage Cargo configuration](storage-overview.md) — the per-crate sqlx feature stripping.
|
||||
Reference in New Issue
Block a user