Skip to content

Commit ccf5cc7

Browse files
committed
Docs: Replace ADRs with colocated decisions
- Deleted all 19 `docs/adr/` files and the ADR creation guide — they were stale and duplicated by the CLAUDE.md pattern - Added `Decision/Why` entries to 11 colocated CLAUDE.md files (check runner, file explorer, listing, commands, font metrics, network, license server, licensing, UI, settings, write operations) - Moved 3 ADRs with rich evidence (metadata tiers, JSON IPC benchmarks, non-reactive file store) to `docs/notes/` and linked from their CLAUDE.md files - Updated AGENTS.md: removed `adr/` from file structure, replaced ADR guide reference with `Decision/Why` guidance - Updated `apps/license-server/README.md` to point at CLAUDE.md instead of deleted ADR files - Skipped ADR 015 (already superseded by 016)
1 parent 0595796 commit ccf5cc7

36 files changed

+200
-1383
lines changed

AGENTS.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ Core structure:
3232
- `website/` - Marketing website (getcmdr.com)
3333
- `/scripts/check/` - Go-based unified check runner (replaces individual scripts)
3434
- `/docs/` - Dev docs
35-
- `adr/` - Architecture decision records
3635
- `guides/` - How-to guides
36+
- `notes/` - Temporary reference notes (benchmarks, analysis) linked from CLAUDE.md files
3737
- `tooling/` - Internal tooling docs
3838
- `architecture.md` - Map of all subsystems with links to their `CLAUDE.md` files ← You probably want to know this!
3939
- `style-guide.md` - Writing and code style rules
@@ -100,7 +100,8 @@ There are two MCP servers available to you:
100100

101101
## Common tasks and reminders
102102

103-
- Capturing decisions: see [here](docs/guides/creating-adrs.md). When choosing between competing tech or processes.
103+
- Capturing decisions: add `Decision/Why` entries to the nearest colocated `CLAUDE.md` file. If the decision has rich
104+
evidence (benchmarks, detailed analysis), put the evidence in `docs/notes/` and link from the CLAUDE.md.
104105
- Adding new dependencies: NEVER rely on your training data! ALWAYS use npm/ncu, or another source to find the latest
105106
versions of libraries. Check out their GitHub, too, and see if they are active. Check Google/Reddit for the latest
106107
best solutions!

apps/desktop/src-tauri/src/commands/CLAUDE.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ immediately to business-logic modules. No significant logic lives here.
4242
- `IpcError` (`{ message: String, timedOut: bool }`) — for commands returning `Result<T, _>`. Use `blocking_result_with_timeout` or map `tokio::time::timeout` errors to `IpcError::timeout()`.
4343
The frontend has matching TypeScript types in `$lib/tauri-commands/ipc-types.ts` (`TimedOut<T>`, `IpcError`, `isIpcError`, `getIpcErrorMessage`). The old `blocking_with_timeout` is kept for `path_exists` and other commands where timeout distinction isn't needed.
4444

45+
**Decision**: JSON for all Tauri IPC, not binary formats (MessagePack, Protobuf).
46+
**Why**: Benchmarked with real directory listings: MessagePack is 34-58% SLOWER than JSON despite being 17-19% smaller. Tauri serializes `Vec<u8>` as a JSON array of numbers, so binary data gets wrapped in JSON anyway, negating size benefits and adding extra decoding overhead. See [benchmark data](../../../../../docs/notes/json-ipc-benchmarks.md).
47+
4548
**Decision**: No `commands/ai.rs` file -- AI commands register directly from `ai::manager` and `ai::suggestions`.
4649
**Why**: The AI subsystem has its own complex lifecycle (model loading, suggestion pipelines). Adding a thin wrapper in `commands/` would just be boilerplate forwarding. Registering directly keeps the AI command surface co-located with its implementation, which changes frequently.
4750

apps/desktop/src-tauri/src/file_system/listing/CLAUDE.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ Frontend Backend
8383
**Decision**: File watcher starts AFTER listing complete
8484
**Why**: Watcher diffs rely on cached entries. Starting before cache is populated would miss initial state.
8585

86+
**Decision**: File metadata tiers — Tier 1-2 eagerly (stat + uid→name), Tier 3-4 deferred.
87+
**Why**: With 50k+ files, each metadata piece has different performance cost. Tier 1 (name, size, dates, permissions) is free from a single `stat()`. Tier 2 (owner name, symlink target) is ~1μs and cacheable. Tier 3 (macOS Spotlight/NSURL metadata) costs ~50-100μs/file. Tier 4 (EXIF, PDF) costs 1-100ms+ and reads file content. See [full tier table](../../../../../../docs/notes/file-metadata-tiers.md).
88+
8689
## Gotchas
8790

8891
**Gotcha**: Background task runs to completion even if cancelled on frontend

apps/desktop/src-tauri/src/file_system/write_operations/CLAUDE.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,11 @@ from pre-computed item sizes. Partial failure is supported: if some items fail,
110110
| `scan-preview-error` | Preview scan failed |
111111
| `scan-preview-cancelled` | Preview scan cancelled |
112112

113+
## Key decisions
114+
115+
**Decision**: Keep `exacl` crate for ACL copy in chunked copies (not custom FFI bindings).
116+
**Why**: `exacl` adds zero new transitive dependencies (all of its deps — `bitflags`, `log`, `scopeguard`, `uuid` — are already in our tree). It provides cross-platform ACL support (macOS, Linux, FreeBSD) and full ACL parsing/manipulation for potential future UI features. The crate appears unmaintained (last release Feb 2024) but ACL APIs are stable and don't change. Our usage is best-effort with graceful fallback — if `exacl` ever breaks, files still copy, they just lose ACLs. MIT licensed (compatible with BSL).
117+
113118
## Dependencies
114119

115120
- `crate::file_system::volume``Volume` trait, `SpaceInfo`, `ScanConflict` (used by `volume_copy`)

apps/desktop/src-tauri/src/font_metrics/CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ METRICS_CACHE: LazyLock<RwLock<HashMap<String, FontMetrics>>>
3939
**Decision**: Frontend measures character widths via Canvas API and ships them to Rust over IPC, rather than Rust measuring fonts directly.
4040
**Why**: Rust has no access to the system's font rendering stack. The browser's Canvas API uses the exact same font rasterizer the user sees, so the measurements match pixel-perfectly. Any Rust-side font library would need to load font files, handle system font resolution, and might produce slightly different widths than what the browser actually renders.
4141

42-
**Decision**: Binary format (bincode2) on disk instead of JSON.
42+
**Decision**: Binary format (bincode2, a maintained fork of the original bincode) on disk instead of JSON.
4343
**Why**: A full Latin character set produces ~4,000 code-point-to-width entries. As JSON that's ~100 KB with key quoting overhead. Bincode compresses this to ~26 KB and deserializes in microseconds vs. milliseconds for JSON parsing. Since this file is only read by Rust (never human-edited), readability doesn't matter.
4444

4545
**Decision**: `RwLock` for the metrics cache instead of `Mutex`.

apps/desktop/src-tauri/src/licensing/CLAUDE.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@ Legacy `activate_license`/`activate_license_async` wrappers still exist for back
7474

7575
## Key decisions
7676

77+
**Decision**: BSL 1.1 license model — free personal use, paid commercial ($59/year or $199 perpetual), converts to AGPL-3.0 after 3 years.
78+
**Why**: The earlier AGPL + trial model felt pushy for hobbyists (trial countdown, nagware, trivial bypass). BSL gives friction-free personal use (no nags ever), clear commercial terms, and simpler enforcement (title bar shows license type). "Source-available" positioning avoids confusing "open source but not really" messaging. Machine IDs are not tracked — one license works on unlimited personal machines.
79+
7780
**Decision**: Ed25519 offline verification with the public key compiled in, rather than server-side-only validation.
7881
**Why**: A file manager must work offline. If license checks required a network call, the app would degrade or nag every time the user is on a plane or behind a restrictive firewall. Offline crypto verification means the license works instantly and permanently, with server calls only needed to check subscription expiry status.
7982

apps/desktop/src-tauri/src/network/CLAUDE.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,25 @@ Discover, browse, and mount SMB network shares. Works on macOS and Linux.
3434

3535
## Key decisions
3636

37+
### `NetFSMountURLAsync` for SMB mounting (not `mount_smbfs` CLI)
38+
39+
Non-blocking (UI stays responsive), credentials passed via secure API (not exposed in process list), native Keychain
40+
integration, and structured error codes instead of parsing stderr. Requires custom Rust FFI bindings for NetFS.framework.
41+
Linux uses `gio mount` (GVFS) instead.
42+
43+
### Custom auth UI with Keychain integration (not system dialog)
44+
45+
Full UX control (login form appears in-pane), smart defaults (pre-fill username from connection history), and
46+
guest/credentials toggle. Uses `security-framework` crate for Keychain access. Passwords never stored in our settings
47+
file — only in Keychain. Linux uses `keyring` crate (Secret Service) with encrypted file fallback.
48+
49+
### `smb-rs` for SMB share enumeration (not `pavao`/libsmbclient or `smbutil`)
50+
51+
MIT license (compatible with BSL, allows dual-licensing for enterprise), pure Rust (no C dependencies), async-native
52+
(built on tokio), and cross-platform (macOS, Linux, Windows). `pavao` (libsmbclient wrapper) was rejected for its GPLv3
53+
license. `smbutil` CLI was rejected for fragile text parsing and process spawning. Fallback to `smbutil`/`smbclient` is
54+
available for older Samba servers where smb-rs's RPC fails.
55+
3756
### Always use IP when available
3857

3958
smb-rs doesn't resolve `.local` hostnames reliably (std lib DNS doesn't handle mDNS). Always pass resolved IP from mDNS discovery. If IP unavailable, use derived hostname (`service_name_to_hostname`).

apps/desktop/src/lib/file-explorer/CLAUDE.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,22 @@ Core explorer UI components:
123123
from DualPaneExplorer via factory pattern
124124
- **rename-flow.svelte.ts** — Rename flow logic (validation, conflict/extension dialogs) extracted from FilePane
125125

126+
## Key decisions
127+
128+
**Decision**: Scoped CSS for file explorer list components, Tailwind elsewhere. **Why**: File lists render 50k+ items.
129+
Scoped CSS produces smaller DOM (no repetitive utility classes on each file entry), enabling faster rendering and lower
130+
memory. Guideline: if a component renders >100 repeated items, prefer scoped CSS.
131+
132+
**Decision**: Icon registry pattern — `iconId` refs in file entries, separate `get_icons()` call, frontend caches.
133+
**Why**: 50k JPEG files would otherwise transmit 50k identical icon blobs (~100-200MB). Instead, file entries carry only
134+
an `iconId` (like `"ext:jpg"`), and a separate IPC call fetches unique icons. Frontend caches icons in IndexedDB across
135+
sessions.
136+
137+
**Decision**: Non-reactive `FileDataStore` — only visible range (~50-100 items) enters Svelte reactivity. **Why**:
138+
Loading 20k+ files into Svelte `$state` causes 9+ second freezes (Svelte tracks the full array internally even with
139+
virtual scrolling). Storing data outside reactivity and slicing only visible items reduces reactivity cost from O(total)
140+
to O(visible). See [benchmarks](../../../../../docs/notes/non-reactive-file-store.md).
141+
126142
## Tabs (`tabs/`)
127143

128144
Each pane has an independent tab bar. Tabs use `{#key}` for clean FilePane recreation on switch (cold load, no warm

apps/desktop/src/lib/settings/CLAUDE.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,14 @@ row intentionally spans the full width.
9999

100100
## Key decisions
101101

102+
### Why hybrid declarative registry with custom UI?
103+
104+
JSON schema → generated UI was rejected (loses per-section UX polish). Manual pages + parser script was rejected (parser
105+
is a second source of truth that drifts on refactors). Full architectural enforcement was overkill for a solo dev +
106+
agents workflow. The hybrid approach gives: search that works perfectly (registry IS the search index), full UX freedom
107+
per section, single source of truth for metadata, and CI catches missing UI or orphaned registry entries automatically.
108+
UI components use Ark UI (see `../ui/CLAUDE.md`).
109+
102110
### Why separate shortcuts from settings?
103111

104112
Settings and shortcuts have different access patterns. Settings are mostly static config loaded at startup. Shortcuts

apps/desktop/src/lib/ui/CLAUDE.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,15 @@ Call `dismissTransientToasts()` on pane navigation to clear stale feedback.
8181
single `command` string prop. Handles clipboard internally (`copyToClipboard` with `navigator.clipboard` fallback).
8282
Parent controls spacing via its own wrapper. Used in `PtpcameradDialog`, `MtpPermissionDialog`, and `ShareBrowser`.
8383

84+
## Ark UI
85+
86+
Uses `@ark-ui/svelte` as the headless component library for complex interactive components (Dialog, Tabs, Select,
87+
Checkbox, Switch, Slider, Radio Group, etc.). Chosen over Bits UI and Melt UI for: 45+ components (vs ~20-25), clean
88+
tree-shaking (1.33 MB unpacked), Zag.js FSM robustness (prevents invalid states), full focus/escape control (disable FSM
89+
defaults with `={false}`, implement custom logic in callbacks), and scoped CSS selectors
90+
(`[data-scope="dialog"][data-part="content"]`) that work with vanilla CSS. Team-maintained by Chakra UI team. Simple
91+
elements like `<Button>` are our own thin wrappers (a button needs no headless library).
92+
8493
## Key decisions
8594

8695
**Decision**: Custom `ModalDialog` with manual overlay + drag logic instead of the native `<dialog>` element. **Why**:

0 commit comments

Comments
 (0)