Files
vdm/docs/01-architecture.md
samiandClaude Opus 5 8bb683b09d scaffold: project structure, wire contract, roadmap and agent briefs
Lays out Velox Download Manager (IDM-class download manager for Ubuntu
26.04) as a monorepo ready for parallel lane development. No implementation
code by design.

- docs/: architecture, roadmap M0-M7, IDM-parity GUI spec, engine design,
  Firefox extension spec, risks/spikes, packaging
- contracts/: wire-contract skeleton (JSON Schema + fixture templates) —
  the single synchronization point between lanes
- docs/agents/: one brief per lane (PROTO, CORE, DAEMON, GUI, EXT, PKG/QA)
  with owned directories, build order and definition of done
- CLAUDE.md: rules of engagement — lane ownership, layering, non-negotiables
- CMake scaffolding with dev/tsan/release/ci presets

Two environment findings shape the design: Firefox here is the Mozilla snap
(native-messaging risk, so the extension carries a loopback-WebSocket
fallback), and Wayland forbids passive clipboard monitoring (so clipboard
capture is explicit-action-first).

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-09 18:21:11 +04:00

101 lines
7.0 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 01 — Architecture
## 1. Decision summary
| Concern | Decision | Why this and not the alternative |
|---|---|---|
| Engine language | **C++23** (`libveloxcore`) | You asked for speed and it is the right call here: segment scheduling, a 64 KiB8 MiB write path, and 32 concurrent sockets are exactly where a GC and a per-chunk allocation tax show up. Rust would be an equally good engine choice, but Qt's C++ API means C++ removes an entire FFI boundary from the GUI lane. |
| HTTP stack | **libcurl** (multi interface, `curl_multi_poll`) | Writing an HTTP/1.1+2 client on raw epoll costs weeks and buys nothing: curl already does connection reuse, HTTP/2 multiplexing, TLS, proxies, SOCKS5, Digest/NTLM auth, redirect and cookie semantics. One `curl_multi` handle per worker thread, N easy handles = N segments. |
| GUI toolkit | **Qt 6 Widgets** (not QML, not GTK4) | IDM's UI is a dense data grid, a toolbar of big icons, a tree, and ~12 tabbed dialogs. `QTreeView` + a custom `QAbstractItemModel` renders 100k rows without breaking a sweat; QML would need that grid hand-built. GTK4/libadwaita fights you the moment you want a non-GNOME look, and gtkmm's tree/column story is worse. Qt also gives you `QSystemTrayIcon`, drag-and-drop, a global clipboard API, and `QSS` theming to get the IDM look. LGPLv3 dynamic linking is fine for an open-source app. |
| Process model | **Daemon + thin clients** | Downloads must survive closing the window, and the extension must work when no window is open. One owner of state also kills every "GUI and extension disagree" bug class. |
| Wire protocol | **JSON-RPC 2.0** over Unix domain socket (+ loopback WebSocket for the extension fallback) | One protocol for GUI, CLI, and extension = one contract to keep in sync, and it is trivially mockable so three lanes can build in parallel. D-Bus is more idiomatic on Linux but is painful to speak from a WebExtension and adds a second schema. A D-Bus *shim* can be added in M5 for desktop integration only. |
| Persistence | **SQLite** (WAL) | Task list, segment bitmaps, categories, rules, queues, history. Crash-safe, zero admin, one file. |
| Extension | **MV3 WebExtension, TypeScript** | Firefox MV3 still allows blocking `webRequest`, which is what makes true "intercept before the browser downloads it" possible — the thing Chrome MV3 took away. |
| Build | **CMake ≥ 3.28 + Ninja + presets** | Presets mean every agent and CI run the identical configure line. |
## 2. Processes
### `veloxd` — the daemon
Owns everything: task list, scheduler, queues, disk, sockets, settings.
- Single instance, enforced by an abstract-namespace lock socket.
- Started by systemd **user** unit `velox.service`, socket-activated by `velox.socket`.
Also auto-started by the GUI or nmhost if not running (`systemctl --user start velox`).
- Listens on:
- `$XDG_RUNTIME_DIR/velox/velox.sock` (mode 0600) — GUI, CLI, nmhost. Peer credentials
checked via `SO_PEERCRED`; same-UID only. No token needed, the socket permission *is*
the authorization.
- `127.0.0.1:<ephemeral, recorded in $XDG_RUNTIME_DIR/velox/ws.port>` — loopback
WebSocket for the extension fallback transport. **Token-authenticated + pairing
prompt.** See `docs/05-extension-spec.md` §4.
- Threads: 1 RPC/event loop, 1 curl-multi transfer thread per ~8 active segments
(capped), 1 disk writer thread per active task, 1 timer thread for the scheduler.
Never block the RPC loop on disk or DNS.
### `velox-gui` — Qt 6 client
Stateless view. On start: connect → `session.hello``download.list` → subscribe to
`event.*`. On daemon restart: exponential-backoff reconnect with a banner, never lose the
window. Everything it displays comes from the daemon; it holds no download state of its own.
### `velox` — CLI
Same RPC, scriptable: `velox add <url> --dir ~/ISOs --segments 16`, `velox ls`,
`velox pause <id>`. Falls out nearly free once the generated client exists, and it is the
fastest way to test the daemon before the GUI is ready.
### `velox-nmhost` — native-messaging bridge
Deliberately trivial and boring: reads Firefox's 4-byte-length-prefixed JSON from stdin,
writes it to the Unix socket, pumps replies back. **No business logic — ever.** Under 300
lines. It exists only because Firefox's native messaging speaks stdio and the daemon
speaks sockets. If a feature needs logic, it belongs in the daemon.
## 3. The layering rule (enforced in review)
```
libveloxcore → knows nothing about RPC, JSON, SQL, or Qt.
Input: a DownloadSpec. Output: bytes on disk + callbacks.
veloxd → knows RPC, SQL, scheduling. Depends on core. No Qt.
velox-gui → knows Qt and the generated client. Zero engine code.
extension → knows the browser and the generated TS client. Zero download logic.
```
If the GUI ever needs to know what a "segment steal" is, the layering has been violated.
The daemon's job is to project the engine's state into the contract's `TaskDetail` type,
and that is the only shape the GUI ever sees.
## 4. How three lanes build at the same time without breaking each other
This is the part that makes parallel agents work, so it is a hard process, not a habit:
1. **`contracts/` is the single source of truth** and is owned by exactly one lane (PROTO).
JSON Schema for every type, method, and event, plus an OpenRPC document.
2. **Codegen, not hand-written types.** `contracts/codegen/gen_cpp.py` emits
`libveloxproto` (structs + (de)serialization); `gen_ts.py` emits `@velox/protocol`.
Generated files are committed so nobody is blocked on running the generator. Hand-editing
a generated file is a merge-blocking offence.
3. **Golden fixtures.** Every method has request/response JSON examples in
`contracts/fixtures/`. `tests/conformance/` replays them against *both* the C++ server
and the TS client. Green fixtures = the two lanes are compatible, without them ever
having run against each other.
4. **`tools/mockd`** — a TS mock daemon that serves the fixtures and fakes progress events.
The GUI and extension lanes develop against it from day one and never wait for `veloxd`.
5. **Protocol changes are a PR to `contracts/` only**, with a `VERSION` bump and updated
fixtures. Adding an optional field = minor. Removing/renaming/retyping = major, and
`session.hello` refuses a mismatched major with a clear error the GUI shows as
"Update Velox".
## 5. Data locations
| What | Path |
|---|---|
| Config | `~/.config/velox/settings.json` |
| Database | `~/.local/share/velox/velox.db` |
| Logs | `~/.local/state/velox/velox.log` (rotated, 5×2 MiB) |
| Runtime sockets | `$XDG_RUNTIME_DIR/velox/` |
| Temp/partial data | configurable; default `~/.local/share/velox/temp/` |
| Default download root | `~/Downloads/` with category subfolders |
Partial files: the target file is created **sparse and preallocated at final size** in the
destination directory as `<name>.veloxpart`, with `<name>.veloxpart.meta` beside it. On
completion the part file is renamed in place — no copy, no second full-size write, and a
half-finished download is never mistaken for a real file by other apps.