ADR-0001: Workspace layout and dependency policy
- Status: accepted
- Date: 2026-09-25
- Status update (Saturday, September 26, 2026): The workspace now has 28 crates and
xtask; the architecture guide maps them. Two dependency rules changed:textweaver-speechalso depends ontextweaver-math, because math is spoken inside the normalization pipeline (ADR-0018), andtextweaver-storedepends ontextweaver-aids(and through it ontextweaver-textandtextweaver-fonts) for the[reading_aids]settings types. Speech still never receives aDocument.espeakng-syswas replaced by hand-written declarations, so the espeak feature needs no clang.live-regionis used by the GUI spike (ADR-0014), not yet bytextweaver-a11y.ureqserves citation lookups (ADR-0019) andrayonbulk conversion (ADR-0016).pdf-extractandpdfium-renderare still listed in the workspace table but no crate uses them (ADR-0010). - Status update (Saturday, September 26, 2026, Phases 1 and 2): seven unused workspace dependencies were removed, among them
pdf-extractandpdfium-render.rust-versionis 1.92, because krilla needs it; the toolchain stays pinned to 1.96.cargo xtask deps --checkchecks the dependency direction in CI: it refuses forbidden edges, and reports two as allowed for now, store on aids and the reader reaching the conversion and citation crates through the app, which exports and inserts citations. espeak-ng is loaded at run time withlibloading, not linked. Removing both exceptions is planned, preferring pure-Rust, in-process crates over subprocesses and C or C++ libraries, and recording each bold choice and its fallback in an ADR. - Status update (Saturday, September 26, 2026): both exceptions are gone. Store depends only on core again: the
[reading_aids]settings types are store's own plain data (textweaver_store::reading_aids), andtextweaver-aidsconverts them. In-reader export, preview, and citations aretextweaver-app'spublishfeature, on by default and in releases; the workspace table declares the app with default features off, and the reader forwardspublishas its own default.cargo xtask deps --checkresolves features and refuses any edge the reader reaches without its default features. A new crate,textweaver-engines, holds the backend registry, so the app no longer depends on each engine crate. There is one notes model (the store's), and font resolution lives intextweaver-fonts. The workspace has 29 crates andxtask.
Context
textweaver reimplements star (Python, 45K lines) in Rust, built by multiple contributors in parallel, so the crate boundaries have to let each one compile and test alone against a stable contract. star's history shows the cost of the opposite: 40+ GUI and TUI mixins sharing state, and a long tail of optional dependencies.
Decision
One Cargo workspace, one crate per job, with a strict dependency direction:
core ← text, speech, keymap, a11y, store, editor
text ← formats
all of the above ← app ← tui, cli
textweaver-coreholds only leaf types shared across crates:CharPos,CharRange,Direction,Bias,Unit,MarkerKind,OffsetMap,Edit,EditOutcome,Rate,Pitch,Volume,Utterance, and the preference enums (Verbosity,PunctuationLevel,HighlightGranularity,CapsIndication). It is centrally maintained; changes are requested rather than made directly.speechdepends only oncore. It receivesUtterances, never aDocument.storedepends only oncore. Keymap overrides are stored as strings and interpreted bykeymap.a11ydepends only oncore. The speech announcer takes a callback, so it does not depend onspeech.editorworks on aropey::Ropepluscore::Edit;text::Document::applyis the document side of the same edit.
Every third-party crate is declared once in [workspace.dependencies] in the root Cargo.toml, and crates opt in with name.workspace = true. That table is the approved list. Adding to it needs review and a stated reason. At Phase 0 it holds: ropey 1.6, unicode-segmentation, regex, pulldown-cmark, scraper, zip, roxmltree, serde, serde_json, toml, directories, thiserror, anyhow, bitflags, log, clap, ratatui, crossterm, espeakng-sys, speech-dispatcher, tts, rodio, proptest, insta, tempfile.
Rules:
- No async runtime in speech: textweaver-speech, the engine backends, and anything the speech thread calls use threads and std::sync::mpsc channels, because engines have thread affinity (SAPI and WinRT COM apartments, Apple engines on the main run loop, espeak-ng's process-wide state; ADR-0003). Other crates may use an async runtime where it clearly helps, such as network-heavy work, as long as it stays out of the speech path (clarified 2026-09-25).
- Performance is a requirement (2026-09-25): measure hot paths with benchmarks before and after optimizing; use rayon for CPU-parallel batch work (conversion, indexing, library scans); an async runtime (tokio) for concurrent I/O outside the speech path (network lookups, many-file reads, watching); streaming or memory-mapped reading (memmap2) for large files instead of reading them whole; caches keyed by content or by path, size, and modification time; allocation-light parsers that borrow from their input. Report timings and memory for anything that handles whole documents or folders.
- thiserror in libraries, anyhow only in binaries (tui, cli, xtask).
- Engines and heavy formats behind cargo features, off by default: textweaver-speech/{espeak, omnivox, speechd, tts-crate}, textweaver-formats/{paperback, pandoc}, textweaver-a11y/live-region.
- Workspace lints: unsafe_code = "deny" (the espeak backend's FFI module opts out with #[allow(unsafe_code)] and a // SAFETY: comment on every block), missing_docs = "warn", clippy all = "warn". CI and -D warnings make all of them errors.
- Edition 2024. Toolchain pinned in rust-toolchain.toml to 1.96 so Windows, CI, and the Docker image agree; rust-version = "1.95" states the true minimum (raised from 1.85 at Integration 1: libloading needs 1.88, File::try_lock 1.89; raised to 1.92 on 2026-09-26 because krilla 0.8, the PDF writer, declares 1.92; raised to 1.94 on 2026-09-28 because rten 0.26, which runs Piper, Whisper, and OCR, declares 1.94, and the nightly MSRV check could no longer resolve the lockfile with 1.92; raised to 1.95 on 2026-10-01 because harper-core 2.11.0, grammar checking, on by default since Wave 7, does not build with 1.94).
Environments. Native Windows and Linux in Docker (docker/Dockerfile, compose.yaml, see docs/dev/docker.md). CI runs fmt, clippy, tests, and rustdoc on Ubuntu, macOS, and Windows; only Ubuntu enables the features that link system libraries.
live-region is not approved for the terminal build. Version 0.3.2 depends on wxDragon, which would pull wxWidgets into every build. It returns with the GUI, behind textweaver-a11y/live-region.
Consequences
- Each contributor can run
cargo test -p <crate>without the others' work. - Moving a type into
coreis the standard fix for a would-be cycle; it is cheap but centralizes change requests for review. - Features that link system libraries (espeak-ng) are exercised only on Linux in CI and Docker; Windows and macOS rely on native backends added later in the project.
See also
- CONTRIBUTING.md: how to build, check, and add a dependency.
- Docker development container: where Linux-only features are built and tested.
- Architecture: the crate map, the threads, and the path from a file to a spoken, highlighted word.
- Documentation index