docs(contributing): add the codebase map and architecture guide

Owner decision D1 keeps docs/ ignored, so the architecture guide lives in
the tracked CONTRIBUTING.md as an extension of Project Boundaries: a map of
lib/, the Rust crates and the native folders, where each feature lives, the
download and playback data flows, recipes for adding a setting, a screen, a
channel method, a lyrics provider and an extension-facing API, test layers,
and code conventions. Folder READMEs written by unit W5-B are linked, not
duplicated.

Facts were taken from the code, not from audits: the lib/ dependency column
comes from scanning every import line per folder, crate edges from each
Cargo.toml, and each data-flow step from reading the call path function by
function. A script checked that every backticked path added here exists
(at the root or as a tracked-file suffix) and that every identifier is found
with a whole-word grep; the remaining tokens are templates or literals that
were confirmed by hand.
This commit is contained in:
zarzet committed 2026-10-19 05:35:08 +07:00
1 parent c6f8ac21b5
commit d3a51a6a36
1 file changed
+327
+327
View File
@@ -148,6 +148,333 @@ Do not add provider-name checks such as `if source == 'provider-name'` to the
main app. The backend should parse and expose the generic declaration, and Dart
should consume that declaration without knowing which extension uses it.
The sections below expand this layout. Folder guides hold the details that are
not repeated here: [android/README.md](android/README.md) (native channel
contract), [rust_backend/README.md](rust_backend/README.md) (crate map),
[test/README.md](test/README.md),
[integration_test/README.md](integration_test/README.md),
[lib/theme/README.md](lib/theme/README.md), and the `SPOTIFLAC_PATCHES.md` file
in each `third_party/` fork, for example
[audio_service](third_party/audio_service/SPOTIFLAC_PATCHES.md).
## Codebase Map
### Flutter (`lib/`)
App files import each other as `package:spotiflac_android/...`. The intended
direction is screens and widgets on top, then controllers, providers, services,
and models and utilities at the bottom. The last column is the current state
found by scanning import lines, including today's exceptions; do not add new
ones.
| Folder | What belongs there | Imports today |
| --- | --- | --- |
| `main.dart`, `app.dart` | Startup, `ProviderScope`, `SpotiFLACApp`, and the `GoRouter` (`/`, `/setup`, `/tutorial`) | Any folder |
| `screens/` | Routes and tab roots; large screens split into `<screen>_*.dart` files; settings pages in `screens/settings/` | Any folder |
| `widgets/` | Reusable widgets and design-system components | Any folder except `screens/`; exceptions: `library_search_results.dart`, `artist_concerts_button.dart`, `mini_player.dart` |
| `controllers/` | UI logic shared by screens and widgets: selection mixins, batch actions, the metadata editor | controllers, widgets, providers, services, models, utils, theme, l10n; `clickable_metadata.dart` also imports `screens/` |
| `providers/` | Riverpod notifiers and state; large notifiers split into `part` files | providers, services, models, utils, constants |
| `services/` | `PlatformBridge`, SQLite stores, the music player, FFmpeg, network storage, notifications | services, models, utils, constants; exceptions: re-export stubs into `providers/` and `controllers/`, and `notification_service.dart` imports `l10n/` |
| `models/` | Data classes and their `json_serializable` output (`*.g.dart`) | models, utils; `library_database_models.dart` imports `sqlite_helpers.dart` from `services/` |
| `utils/` | Small helpers, pure policies, and the settings search catalog | utils, models, constants, l10n; seven files import `services/` (for example `file_access.dart`, `logger.dart`), `saf_download_tree.dart` imports `providers/`, three re-export stubs point into `controllers/` |
| `theme/` | `AppTheme`, `AppTokens`, motion, icons, and the Mornye theme | theme, models; `dynamic_color_wrapper.dart` imports `providers/`, `cover_palette.dart` imports `widgets/` and `services/` |
| `l10n/` | ARB sources in `arb/`, the `context.l10n` extension in `l10n.dart`, and `supported_locales.dart`; `app_localizations*.dart` is generated | utils |
| `constants/` | App info, language choices, `MusicServices` IDs | l10n |
| `routes/` | `now_playing_route.dart` | services, theme |
No provider or service imports `screens/` or `widgets/`. Among services, only
the navigation, orientation, music player, and player widget services import
Flutter's `material.dart` or `widgets.dart`; among providers, the preview
player, extension, download queue, download queue policy and theme providers
do.
### Rust (`rust_backend/crates/`)
Workspace dependencies point only downward:
| Crate | Package | Role | Depends on |
| --- | --- | --- | --- |
| `core/` | `spotiflac-core` | Shared logic without bindings: tags, lyrics parsing and provider config, metadata, filenames, CUE, covers | none |
| `network/` | `spotiflac-network` | Shared, cancellable HTTP transport | none |
| `providers/` | `spotiflac-providers` | Built-in clients: Deezer, MusicBrainz, platform resolver, lyrics | core, network |
| `extensions/` | `spotiflac-extensions` | Extension JavaScript runtime (rquickjs), host functions, manager, repository, and backend operations such as downloads, library scan, and tags | core, network, providers |
| `mobile/` | `spotiflac-mobile` | UniFFI API, mostly methods on `ExtensionManager`; built as `cdylib` and `staticlib`; Kotlin package `com.spotiflac.backend`, Swift module `SpotiFLACBackend` | core, network, extensions |
| `bindgen/` | `spotiflac-bindgen` | UniFFI bindgen CLI used by `scripts/build_rust_backend.sh` | none |
### Native
Android Kotlin lives in `android/app/src/main/kotlin/com/zarz/spotiflac/`.
`android/app/build.gradle.kts` adds `android/app/src/rust/kotlin/com/zarz/spotiflac/`
and the generated bindings to the same source set.
| Files | Role |
| --- | --- |
| `MainActivity.kt` | Backend channel `com.zarz.spotiflac/backend`, the `nativeBackendMethods` set, and the `when (call.method)` dispatch |
| `MainActivity*Calls.kt`, `MainActivitySaf*.kt`, `MainActivityProgressStreams.kt`, `MainActivitySmallChannels.kt` | `MainActivity.handleX(call, result)` extension functions grouped by area, progress EventChannels, small channels |
| `DownloadService*.kt`, `BackgroundWorkService.kt` | Foreground services: the native download worker and its snapshot, other background work |
| `NativeDownloadFinalizer.kt`, `NativeFinalizer*.kt`, `NativeHistoryStore.kt` | Post-download finalization and the native history write |
| `Saf*.kt` | Storage Access Framework helpers |
| `UsbBitPerfectPlugin.kt`, `Usb*.kt`, `BitPerfectPcm.kt`, `Dsd*.kt`, `HiResPlayback.kt`, `NativeAudio.kt` | Bit-perfect and USB playback (`com.zarz.spotiflac/usb_pcm`) |
| `PlayerWidget*.kt`, `DownloadQueueWidgetProvider.kt` | Home-screen widgets |
| `CoreBackend.kt`; `SelectedCoreBackend.kt`, `RustCoreBackendApplication.kt`, `NativeDataJobs.kt` | The `CoreBackend` interface and its Rust implementation `RustCoreBackend`, which maps method names to UniFFI calls |
iOS code lives in `ios/Runner/`: `AppDelegate.swift` (backend channel,
`handleMethodCall` and its `osMethods` set, progress EventChannels, background
tasks), `CoreBackend.swift` (Rust calls through `SpotiFLACBackend`,
`invokeApplication`), and smaller bridges such as `PlayerWidgetBridge.swift`
and `ConcertCalendarBridge.swift`. The WidgetKit extension is in
`ios/PlayerWidget/`.
## Where Each Feature Lives
Dart paths are under `lib/`, Rust paths under `rust_backend/crates/`, and
Kotlin files in the Android folders above.
| Feature | Dart | Rust | Native |
| --- | --- | --- | --- |
| Search and URL import | `providers/track_provider.dart` (`TrackNotifier`), `controllers/home_search_logic.dart` | `extensions/src/backend/provider_metadata.rs`, `mobile/src/metadata.rs` | `RustCoreBackendApplication.kt`, `CoreBackend.swift` |
| Download | `providers/download_queue_provider.dart` and its `download_queue_provider_*.dart` parts, `services/download_request_payload.dart` | `extensions/src/backend/downloads.rs`, `mobile/src/manager.rs` | `DownloadService.kt`, `SafDownloadHandler.kt`, `NativeDownloadFinalizer.kt` |
| Queue persistence and history | `services/download_queue_persistence.dart`, `providers/download_history_provider.dart`, `services/history_database.dart`, `services/history_schema.dart` | none | `NativeHistoryStore.kt`, `NativeFinalizerHistory.kt`, `DownloadServiceSnapshot.kt` |
| Local library scan | `providers/local_library_provider.dart` and its `local_library_provider_*.dart` parts, `services/library_database.dart` | `extensions/src/backend/library/scan.rs`, `mobile/src/tags.rs`, `mobile/src/native_library.rs` | `MainActivityLibraryScanCalls.kt`; SAF trees in `MainActivitySafScan.kt` and the `Saf*.kt` files |
| Playback and queue | `providers/playback_provider.dart`, `providers/music_player_provider.dart`, `services/music_player_service.dart` (automix and autoplay parts), `services/music_player_runtime.dart`, `services/music_playback_deck.dart` | AutoMix analysis: `core/src/native_audio_analysis.rs` via `mobile/src/data_jobs.rs` | `UsbBitPerfectPlugin.kt`; forks in `third_party/audio_service`, `third_party/audioplayers_android`, `third_party/audioplayers_darwin` |
| Lyrics | `utils/lyrics_parser.dart`, `utils/lyrics_metadata_helper.dart`, `controllers/player_metadata_controller.dart` | `providers/src/lyrics/`, `core/src/lyrics/`, `extensions/src/lyrics.rs`, `mobile/src/lyrics.rs`, `mobile/src/native_lyrics.rs` | `SelectedCoreBackend.kt`, `CoreBackend.swift` |
| Metadata editing and conversion | `controllers/metadata_editor_controller.dart`, `screens/track_metadata_edit_sheet.dart`, `screens/track_metadata_convert.dart`, `services/ffmpeg_service.dart` (FFmpeg Kit through `services/ffmpeg_support.dart`) | `core/src/tags/`, `extensions/src/backend/tags.rs`, `mobile/src/tags.rs` | `MainActivityMetadataCalls.kt` |
| Extensions and repository | `providers/extension_provider.dart`, `providers/repo_provider.dart`, `models/extension.dart`, `screens/settings/extensions_page.dart`, `screens/repo_tab.dart` | `extensions/src/manifest.rs`, `extensions/src/runtime.rs`, `extensions/src/manager/`, `extensions/src/repository/`, `mobile/src/extensions.rs`, `mobile/src/repository.rs` | `RustCoreBackendApplication.kt`, `ExtensionAuthCallbacks.kt`, `MainActivityExtensionIntents.kt`, `ExtensionCallbackParser.swift` |
| Settings | `providers/settings_provider.dart`, `models/settings.dart`, `screens/settings/`, `utils/settings_search_catalog.dart` | none; values the backend needs are pushed by the `_sync*ToBackend` methods of `SettingsNotifier` | none |
| Statistics and Rewind | `providers/listening_statistics_provider.dart`, `services/listening_statistics.dart`, `providers/listening_rewind_provider.dart`, `services/listening_rewind.dart` | none | none |
| Widgets and notifications | `services/player_widget_service.dart`, `services/notification_service.dart`, `services/playback_notification.dart`, `providers/playback_notification_provider.dart` | none | `PlayerWidgetProvider.kt`, `DownloadQueueWidgetProvider.kt`, notifications in `DownloadService.kt`, `PlayerWidgetBridge.swift`, `ios/PlayerWidget/` |
## Data Flow
### Download (Android native worker)
1. `downloadSingleTrack` (`lib/widgets/track_detail_actions.dart`) calls
`DownloadQueueNotifier.addToQueue`, which appends a `DownloadItem`, saves
the queue, and schedules `_processQueue`.
2. `_processQueueSingleFlight` (`lib/providers/download_queue_provider.dart`)
checks storage and network mode, then calls
`_tryProcessQueueWithAndroidNativeWorker`
(`lib/providers/download_queue_provider_native_worker.dart`). It proceeds
only when `_canUseAndroidNativeWorker` holds: Android,
`nativeDownloadWorkerEnabled` and `useExtensionProviders` on, an enabled
extension with a download provider, a download tree URI when SAF storage is
in use, and no network-folder items.
3. It builds the first request with `_buildAndroidNativeWorkerRequest` and calls
`PlatformBridge.startNativeDownloadWorker`, which writes requests and
settings to JSON files and sends their paths. Remaining items follow with
`appendNativeDownloadWorkerRequests` and
`finishNativeDownloadWorkerPreparation`.
4. `MainActivity.handleStartNativeDownloadWorker` (`MainActivityServiceCalls.kt`)
calls `DownloadService.startNativeQueueFromFiles`, which starts the
foreground service. `onStartCommand` runs `startNativeWorker`, then
`runNativeWorkerConcurrent` and `processConcurrentNativeRequest` per item.
5. `SafDownloadHandler.handle` calls `CoreBackend.downloadByStrategy`. The Rust
implementation in `SelectedCoreBackend.kt` calls the UniFFI
`ExtensionManager.downloadByStrategy` (`mobile/src/manager.rs`), which runs
`download_by_strategy` in `extensions/src/backend/downloads.rs`.
6. On success, `NativeDownloadFinalizer.finalize` finishes the file and, when
`save_download_history` is set, writes the row with
`NativeHistoryStore.upsert`. `writeNativeWorkerSnapshot`
(`DownloadServiceSnapshot.kt`) publishes item state.
7. `NativeDownloadWorkerMonitor.run` polls
`PlatformBridge.getNativeDownloadWorkerSnapshot`.
`_applyAndroidNativeWorkerSnapshot` updates the items and acknowledges them
with `acknowledgeNativeDownloadWorkerItems`; `_completeAndroidNativeWorkerItem`
calls `reconcileNativeDownloadHistory` (`lib/services/native_download_history.dart`)
so `DownloadHistoryNotifier` adopts or reloads the native row.
Without the native worker (iOS, or when the gate fails), items run through
`_downloadSingleItem` (`lib/providers/download_queue_provider_single_item.dart`),
which calls `PlatformBridge.downloadByStrategy`, finalizes in Dart, and records
history with `DownloadHistoryNotifier.addToHistory`. Android handles that call
in `handleDownloadByStrategy`; iOS in `CoreBackend.swift`.
### Playback
1. A screen calls `PlaybackController` (`lib/providers/playback_provider.dart`),
for example `playTrackList` from `library_tracks_folder_screen.dart`. When
`playerMode` is not `internal` and the source is not `network://`, it opens
the file in an external app instead.
2. `MusicPlayerController.playAll` (`lib/providers/music_player_provider.dart`)
calls `ensureInitialized`, which runs `MusicPlayerRuntime.initialize`
(`lib/services/music_player_runtime.dart`),
which creates the `MusicPlayerHandler` through `AudioService.init` from the
`third_party/audio_service` fork, then calls `setQueueAndPlay`.
3. `MusicPlayerHandler.setQueueAndPlay` (`lib/services/music_player_service.dart`)
publishes `queue`, then `_playIndex` and `_loadIndex` publish `mediaItem`
and call `_startResolvedSource`.
4. `_startResolvedSource` calls `MusicPlaybackDeck.setSource` or
`setNetworkSource` and `resume` (`lib/services/music_playback_deck.dart`).
The deck wraps an audioplayers `AudioPlayer`, served by the
`third_party/audioplayers_android` and `third_party/audioplayers_darwin`
overrides; with USB bit-perfect enabled on Android it first tries to
prepare the file through `UsbBitPerfectPlugin.kt`.
5. Player events reach `_broadcastState`, which updates `playbackState`.
`MusicPlayerRuntime.playbackStateEvents`, `mediaItemEvents`, and
`queueEvents` feed `playbackStateProvider`, `currentMediaItemProvider`, and
`playQueueProvider`, which the mini player and Now Playing screen watch.
## How to Add Things
### A Setting
- Add the field and its default to `AppSettings` (`lib/models/settings.dart`,
constructor and `copyWith`), then run `build_runner` (see
[Generated Files](#generated-files)) to refresh `settings.g.dart`.
- `SettingsNotifier` stores `AppSettings.toJson()` as one JSON string under the
SharedPreferences key `app_settings`, so the JSON key is the Dart field name.
Renaming a field changes the stored key. Use a `@JsonKey(fromJson: ...)`
helper to clamp stored values, as the AutoMix fields do.
- Add a setter to `SettingsNotifier` (`lib/providers/settings_provider.dart`):
`state = state.copyWith(...)` followed by `_saveSettings()`. If the backend
needs the value, push it as `_syncLyricsSettingsToBackend` does.
- Rewrites of existing stored values go in `_runMigrations`: bump
`_currentMigrationVersion` and guard the step with `if (lastMigration < N)`.
- Add the row to the owning page in `lib/screens/settings/` and a
`SettingsSearchEntry` to the matching list in
`lib/utils/settings_search_catalog.dart`.
- Add strings to `lib/l10n/arb/app_en.arb` with an `@key` description and read
them through `context.l10n`.
- Test with the JSON round trip in `test/app_settings_model_test.dart`, setter
and persistence cases like `test/settings_persistence_test.dart` (a
`SettingsNotifier` subclass over a fake `SharedPreferences`), and
`test/settings_search_test.dart`.
### A Screen
- Put it in `lib/screens/` (settings pages in `lib/screens/settings/`) and split
a large screen into `<screen>_*.dart` files.
- Push it with `Navigator.push` and a `MaterialPageRoute` or `slidePageRoute`
(`lib/widgets/animation_utils.dart`). Tabs keep their own `Navigator` from
`_TabNavigator` in `lib/screens/main_shell.dart`; `GoRouter` in `lib/app.dart`
owns only `/`, `/setup`, and `/tutorial`.
- Use the shared components: `AppSliverHeader`, `showAppBottomSheet` and
`AppSheetHandle`, `SettingsGroup`, and the `AppTokens` scale.
- A track collection screen uses `CollectionScaffold` and `SelectionModeMixin`,
and its file name joins the `screens` list in the "every collection screen
goes through the shared shell" test.
- `test/design_system_test.dart` scans every file in `lib/`: each `IconButton`
and `AppIconButton` passes `tooltip`, and only allowlisted files may contain
`expandedTitleScale` (`app_sliver_header.dart`), a hand-built drag handle
(`app_bottom_sheet.dart`), a modal sheet shape (`app_theme.dart`), or
`OverlayEntry(` (`selection_bottom_bar.dart`, `app_menu_button.dart`).
### A Platform Channel Method
- Dart: add a static method to `PlatformBridge` in
`lib/services/platform_bridge.dart` that calls `_channel.invokeMethod` with
snake_case argument keys; helper classes and codecs can go in a part under
`lib/services/platform_bridge/`. Decode JSON results with
`_decodeJsonResultAsync` or `_decodeRequiredMapResultAsync`.
- Rust-backed methods: on Android a name outside `nativeBackendMethods` goes
through `dispatchBackendApplication` (`SelectedCoreBackend.kt`) to
`RustCoreBackend.dispatchApplication` (`RustCoreBackendApplication.kt`); add
the case there. On iOS add it to `invokeApplication` in
`ios/Runner/CoreBackend.swift`.
- Android-only methods: add the name to `nativeBackendMethods` and a case to
the `when (call.method)` block in `MainActivity.kt`, calling a
`MainActivity.handleX` function in the matching `MainActivity*Calls.kt` file.
`MainActivityRoutingTest.kt` fails when the set and the cases differ.
- iOS-only methods: add the name to `osMethods` in `handleMethodCall`
(`ios/Runner/AppDelegate.swift`) and handle it there or in `invokeOsMethod`.
`test/source_contracts_test.dart` pins parts of that file.
- Large JSON: `bridgeJsonResult` in `MainActivity.kt` and `AppDelegate.swift`
writes a result of 256 KiB or more to a temporary file and returns
`{"__json_file": "<path>"}`; Dart reads and deletes it. Use it for any result
that can grow with the library.
- New Rust entry points are `#[uniffi::export]` items in
`rust_backend/crates/mobile/src/`; the build regenerates bindings (see
[Development Setup](#development-setup)).
- Dart tests stub `com.zarz.spotiflac/backend` with `setMockMethodCallHandler`,
for example `test/extension_initialization_test.dart`.
### A Lyrics Provider
- Provider-specific sources fit an extension first: installed lyrics
extensions appear in the provider order as `extension:<id>` entries, and
`InstalledLyricsFetcher` (`rust_backend/crates/extensions/src/lyrics.rs`)
lists and calls them.
- A built-in provider is a `pub(super) fn` on `BuiltinLyricsClient` in a file
under `rust_backend/crates/providers/src/lyrics/builtin/` (`apple.rs`,
`direct.rs`, `proxy.rs`, `ttml.rs`), taking `&SearchRequest` and
`&Check<'_>` and returning `Result<LyricsResponse, LyricsError>`.
- Add its match arm to `BuiltinLyricsClient::fetch_once`
(`rust_backend/crates/providers/src/lyrics/builtin.rs`).
- Add the ID to `BUILTIN_PROVIDERS` and `available_providers` in
`rust_backend/crates/core/src/lyrics/config.rs`; `normalize_provider_order`
drops unknown IDs. `DEFAULT_PROVIDERS` mirrors the Dart default
`AppSettings.lyricsProviders`; changing either for existing users needs a
settings migration (see Migration 13 in `_runMigrations`).
- Dart UI: `_builtInProviderIds` and `_getLyricsProviderInfo` in
`lib/screens/settings/lyrics_provider_priority_page.dart`,
`_providerDisplayNames` in `lib/screens/settings/lyrics_settings_page.dart`,
and a description key like `lyricsProviderLrclibDesc`.
- Tests: `rust_backend/crates/providers/src/lyrics/service/tests.rs`, plus the
shared usability cases described in [Validation](#validation).
### An Extension-Facing API
- The manifest contract is `rust_backend/crates/extensions/src/manifest.rs`.
Host functions are registered by `register` in
`rust_backend/crates/extensions/src/host.rs` and the `*_host.rs` modules; the
JavaScript side is `rust_backend/crates/extensions/src/prelude.js`.
- Surface results as generic fields, such as `has_download_provider` from
`rust_backend/crates/extensions/src/manager/info.rs` read by
`Extension.fromJson` (`lib/models/extension.dart`), never as extension-name
checks (see [Project Boundaries](#project-boundaries)).
- Treat existing host functions and manifest fields as a contract with
installed extensions; add new ones rather than changing them.
- Tests: `rust_backend/crates/extensions/src/runtime/tests.rs`,
`rust_backend/crates/extensions/src/manager/tests.rs`,
`rust_backend/crates/extensions/tests/`, and the `test/extension_*_test.dart`
files.
- Keep tests provider-neutral: generic extension IDs, display names,
namespaces, and example URLs; no copied extension source and no dependency
on the separate extension repository. Platform names stay only where the
fixed API, metadata schema, URL parsing, or legacy compatibility needs them.
## Testing Each Layer
Commands are in [Validation](#validation).
| Layer | Location | Notes |
| --- | --- | --- |
| Dart unit and widget | `test/` | [test/README.md](test/README.md); fixtures in `test/fixtures/`, helpers in `test/support/` |
| Device and performance | `integration_test/` | [integration_test/README.md](integration_test/README.md) |
| Kotlin JVM | `android/app/src/test/kotlin/com/zarz/spotiflac/` | Case tables in `android/app/src/test/resources/` are also read by Dart tests |
| Rust | `tests.rs` modules beside the code, `rust_backend/crates/extensions/tests/` | For example `rust_backend/crates/extensions/src/runtime/tests.rs` |
| iOS | `ios/RunnerTests/` | `RunnerTests.swift` |
## Conventions
- Split large Dart classes with `part` files: the library declares
`part 'name_area.dart';`, the part starts with `part of 'name.dart';` and
holds `extension _Area on Name { ... }` or private helper classes. Parts that
use the notifier's `state` start with
`// ignore_for_file: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member`,
as most `download_queue_provider_*.dart` parts do.
- Public notifier methods stay on the class. More than 120 test classes
subclass `DownloadQueueNotifier`, `DownloadHistoryNotifier`,
`LocalLibraryNotifier`, `LibraryCollectionsNotifier`, `ExtensionNotifier`,
and `SettingsNotifier` to override them; only private members move to parts.
- After moving a file, leave a one-line `export` of the new path at the old one
while other files import it, as `lib/services/batch_track_actions.dart` does.
- Source-text contracts: `test/source_contracts_test.dart` pins literal text in
files such as `ios/Runner/AppDelegate.swift`, `NativeDownloadFinalizer.kt`,
`NativeHistoryStore.kt`, `DownloadServiceSnapshot.kt`, and
`lib/services/history_schema.dart`; `test/design_system_test.dart` pins file
names; `MainActivityRoutingTest.kt` reads `MainActivity.kt`. When code moves,
update the path or assertion in the same commit and never delete the check.
- Keep hand-written files under about 800 lines, split by responsibility.
Exceptions: generated or frozen tables such as
`rust_backend/crates/core/src/metadata/musicbrainz/case_data.rs`, the
settings search catalog `lib/utils/settings_search_catalog.dart`, upstream
code in `third_party/` and `rust_backend/vendor/`, ARB files, and generated
`*.g.dart` and localization files.
- Never write the em dash character (U+2014) in code, comments, strings, docs,
or commit messages; use a comma, colon, parentheses, or a new sentence.
- Commit messages follow [Code and Commit Style](#code-and-commit-style).
## Generated Files
- After changing ARB files, run `fvm flutter gen-l10n` (or `fvm flutter pub get`)