diff --git a/CMakeLists.txt b/CMakeLists.txt
index e79039c..894307f 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -132,8 +132,17 @@ install(TARGETS kastword RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT K
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/io.github.shape_machine.Kastword.desktop
DESTINATION ${CMAKE_INSTALL_DATADIR}/applications COMPONENT Kastword)
install(FILES data/io.github.shape_machine.Kastword.metainfo.xml DESTINATION share/metainfo COMPONENT Kastword)
-install(FILES README.md LICENSES/GPL-3.0-or-later.txt
- DESTINATION share/doc/kastword COMPONENT Kastword)
+install(FILES README.md DESTINATION share/doc/kastword COMPONENT Kastword)
+install(FILES docs/DEVELOPMENT.md docs/AUTOMATIC_PASTE.md
+ DESTINATION share/doc/kastword/docs COMPONENT Kastword)
+install(FILES
+ screenshots/01-offline-dictation.png
+ screenshots/02-speech-models.png
+ screenshots/03-audio-input.png
+ screenshots/04-settings.png
+ DESTINATION share/doc/kastword/screenshots COMPONENT Kastword)
+install(FILES LICENSES/GPL-3.0-or-later.txt
+ DESTINATION share/doc/kastword/LICENSES COMPONENT Kastword)
if(KASTWORD_FETCH_DEFAULT_MODEL)
install(FILES "${KASTWORD_DEFAULT_MODEL}" DESTINATION share/kastword/models COMPONENT Kastword)
endif()
@@ -248,6 +257,10 @@ if(BUILD_TESTING)
kastword_enable_sanitizers(kastword_screenshot_platform_test)
add_test(NAME screenshot-platform COMMAND kastword_screenshot_platform_test)
+ add_test(NAME documentation-links
+ COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/tools/check-documentation-links.sh
+ ${CMAKE_CURRENT_SOURCE_DIR})
+
add_test(NAME translation-catalog
COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/tests/check-translations.sh)
set_tests_properties(translation-catalog PROPERTIES
diff --git a/Makefile b/Makefile
index 345ed9d..de5b019 100644
--- a/Makefile
+++ b/Makefile
@@ -81,6 +81,17 @@ install-smoke: ensure-configured
"$$smoke_prefix/share/metainfo/io.github.shape_machine.Kastword.metainfo.xml"; \
test -f "$$smoke_prefix/share/locale/x-test/LC_MESSAGES/kastword.mo"; \
test ! -e "$$smoke_prefix/share/kastword/models/ggml-base.en.bin"; \
+ test -f "$$smoke_prefix/share/doc/kastword/README.md"; \
+ test -f "$$smoke_prefix/share/doc/kastword/docs/DEVELOPMENT.md"; \
+ test -f "$$smoke_prefix/share/doc/kastword/docs/AUTOMATIC_PASTE.md"; \
+ test -f "$$smoke_prefix/share/doc/kastword/screenshots/01-offline-dictation.png"; \
+ test -f "$$smoke_prefix/share/doc/kastword/screenshots/02-speech-models.png"; \
+ test -f "$$smoke_prefix/share/doc/kastword/screenshots/03-audio-input.png"; \
+ test -f "$$smoke_prefix/share/doc/kastword/screenshots/04-settings.png"; \
+ test -f "$$smoke_prefix/share/doc/kastword/LICENSES/GPL-3.0-or-later.txt"; \
+ ./tools/check-documentation-links.sh "$$smoke_prefix/share/doc/kastword"; \
+ legacy_license="$$smoke_prefix/share/doc/kastword/GPL-3.0-or-later.txt"; \
+ cmake -E touch "$$legacy_license"; \
find "$$smoke_prefix" -type f -print > "$$installed_files"; \
user_model="$$smoke_prefix/share/kastword/models/ggml-base.en.bin"; \
cmake -E make_directory "$$(dirname "$$user_model")"; \
@@ -97,7 +108,14 @@ uninstall:
"$(PREFIX)/share/metainfo/io.github.shape_machine.Kastword.metainfo.xml" \
"$(PREFIX)/share/locale/x-test/LC_MESSAGES/kastword.mo" \
"$(PREFIX)/share/doc/kastword/README.md" \
- "$(PREFIX)/share/doc/kastword/GPL-3.0-or-later.txt"
+ "$(PREFIX)/share/doc/kastword/docs/DEVELOPMENT.md" \
+ "$(PREFIX)/share/doc/kastword/docs/AUTOMATIC_PASTE.md" \
+ "$(PREFIX)/share/doc/kastword/screenshots/01-offline-dictation.png" \
+ "$(PREFIX)/share/doc/kastword/screenshots/02-speech-models.png" \
+ "$(PREFIX)/share/doc/kastword/screenshots/03-audio-input.png" \
+ "$(PREFIX)/share/doc/kastword/screenshots/04-settings.png" \
+ "$(PREFIX)/share/doc/kastword/GPL-3.0-or-later.txt" \
+ "$(PREFIX)/share/doc/kastword/LICENSES/GPL-3.0-or-later.txt"
@if command -v update-desktop-database >/dev/null 2>&1; then \
update-desktop-database "$(PREFIX)/share/applications"; \
fi
diff --git a/README.md b/README.md
index 089399d..132e1f0 100644
--- a/README.md
+++ b/README.md
@@ -9,30 +9,82 @@ SPDX-License-Identifier: GPL-3.0-or-later
**Private, offline dictation built for KDE Plasma.**
-> [!WARNING]
-> Kastword is early alpha software. This repository currently provides source code only—there
-> are no supported binary releases or stable compatibility guarantees yet.
-
-Kastword records speech, transcribes it locally with `whisper.cpp`, and pastes the result into the
-application you were using. No dictated audio or text is sent to a cloud service. After first-run
-setup, Kastword stays out of the way in the system tray until you press the configurable global
-shortcut, **Meta+Z** by default.
+Kastword records your voice, transcribes it locally with Whisper, and places the result on your
+clipboard. It can optionally paste the text into the application you were using. Dictated audio and
+text are never sent to a cloud transcription service.
-Kastword is an independent project built with KDE technology; it is not currently an official KDE
-project or endorsed by KDE e.V.
+> [!WARNING]
+> Kastword is early alpha software. This repository currently provides source code only—there are
+> no supported binary releases or stable compatibility guarantees yet.
+
+
+
+## Why Kastword?
+
+- **Local transcription:** speech recognition runs on your computer using a model you choose.
+- **Tray-first workflow:** start and stop dictation with a configurable KDE global shortcut,
+ **Meta+Z** by default.
+- **Your microphone, your choice:** follow the system default, select a specific device, or disable
+ microphone access from the application. Kastword handles downmixing, resampling, and device
+ hot-plug events without silently replacing a disconnected specific device.
+- **Manage models in the app:** download, verify, resume, switch, inspect, or remove supported
+ English-only and multilingual Whisper models.
+- **Useful anywhere:** copy text to the clipboard, then paste manually or enable optional automatic
+ paste on Plasma Wayland or X11.
+- **Configurable limits and output:** choose the transcription language, recording limit, paste
+ shortcuts, and global keyboard shortcut.
+- **No history or telemetry:** Kastword does not save recordings, retain a transcription history,
+ or include usage tracking.
+
+## How dictation works
+
+1. Start Kastword and explicitly download a recommended Whisper model, or select a compatible
+ model already on your computer.
+2. Focus the text field or terminal where you want the result.
+3. Press **Meta+Z**, speak, then press it again.
+4. Kastword transcribes the recording locally and updates the clipboard.
+5. Paste manually, or let Kastword send your configured paste shortcut when automatic paste is
+ enabled and its helper is available.
+
+The window can be opened or hidden from the tray icon. The tray menu can also start or stop
+dictation and quit the application.
-## What works today
+Dictation remains disabled until both a valid model and an available audio input are selected. The
+Speech Models view shows model disk usage and can resume or retry interrupted downloads. If a
+specifically selected microphone disconnects, Kastword waits for it to return or for you to choose
+another input instead of silently switching devices.
-- configurable KDE global dictation shortcut
-- tray-first operation with recording, transcription, and success indicators
-- selectable Qt Multimedia audio input with hot-plug recovery, downmixing, and resampling
-- completely local transcription using a downloaded or user-selected compatible Whisper model
-- clipboard output with optional automatic paste
-- X11 paste through `xdotool`
-- Plasma Wayland paste through `ydotool`
-- no saved recordings, transcription history, or telemetry
+## Privacy and trust
-## Current status
+- Microphone samples remain in process memory only until transcription completes. Recordings stop
+ at the configured duration limit—five minutes by default—or a 256 MiB raw-audio ceiling.
+- Raw audio and transcription history are not written to disk.
+- Transcribed text is placed on the desktop clipboard and primary selection. Clipboard managers,
+ target applications, desktop services, crash dumps, and the operating system may retain it
+ independently of Kastword.
+- Runtime network access occurs only when you explicitly request a model download. Downloads use
+ immutable HTTPS URLs and are activated only after size, format, and SHA-256 verification.
+ Transcription itself remains offline.
+- Downloaded models remain in per-user local storage under `~/.local/share/kastword/models/` by
+ default. Custom model files are trusted input parsed inside Kastword, so use models from sources
+ you trust.
+- Automatic paste is disabled by default and relies on separately installed desktop helpers. See
+ [Automatic paste setup and security](docs/AUTOMATIC_PASTE.md).
+- Kastword refuses to run with elevated privileges and contains no telemetry.
+
+The clear-transcription action removes Kastword's retained text and clears matching current
+clipboard selections. It cannot erase entries already retained by clipboard-manager history.
+
+## Platform status
| Environment | Status |
| --- | --- |
@@ -42,239 +94,37 @@ project or endorsed by KDE e.V.
| Plasma X11 | Implemented but needs broader testing |
| Other distributions and desktops | Community testing needed |
-## Install dependencies on CachyOS or Arch Linux
-
-```sh
-sudo pacman -S --needed \
- base-devel cmake ninja git \
- qt6-base qt6-declarative qt6-multimedia \
- extra-cmake-modules kirigami kdeclarative \
- kconfig kcoreaddons kdbusaddons kglobalaccel ki18n \
- knotifications kstatusnotifieritem
-```
-
-Install the optional paste helper for your session:
-
-```sh
-# Plasma Wayland
-sudo pacman -S --needed ydotool
-
-# Plasma X11
-sudo pacman -S --needed xdotool
-```
-
-### Enable automatic paste on Plasma Wayland
-
-Installing `ydotool` provides both the command-line client and the `ydotoold` user service. Enable
-and start the service for the current user:
-
-```sh
-systemctl --user enable --now ydotool.service
-systemctl --user is-active ydotool.service
-```
-
-The second command must print `active`. The daemon creates a virtual keyboard through
-`/dev/uinput` and listens on a socket in the user's runtime directory. Verify both are present and
-that the client can connect:
-
-```sh
-ls -l /dev/uinput "$XDG_RUNTIME_DIR/.ydotool_socket"
-ydotool debug
-```
-
-Finally, test actual input delivery. Run the following command, immediately focus an editable text
-field, and wait one second; `Kastword ydotool test` should appear there:
-
-```sh
-sleep 1 && ydotool type 'Kastword ydotool test'
-```
-
-If the service is inactive or the socket is missing, inspect its log:
-
-```sh
-systemctl --user status ydotool.service
-journalctl --user -u ydotool.service -b
-```
-
-Errors mentioning `/dev/uinput`, the daemon socket, or permission denied mean the helper is not
-usable by the logged-in user. Check that the CachyOS/Arch package is current, restart the user
-service, and log out and back in after changing device or group permissions. Avoid running Kastword
-or `ydotool` with `sudo`.
-
-Kastword never requests elevated permissions and does not start or configure `ydotoold` itself. If
-the helper is unavailable, transcription is still copied to the clipboard for manual pasting.
-
-## Build and run
+Kastword is an independent project built with KDE technology; it is not currently an official KDE
+project or endorsed by KDE e.V.
-The first default build downloads one immutable, pinned dependency:
+## Build from source
-- `whisper.cpp` source at commit `a91dd3be72f70dd1b3cb6e252f35fa17b93f596c`
+There are no supported binary packages yet. On a supported development system, install the
+[documented dependencies](docs/DEVELOPMENT.md#dependencies), then run:
```sh
make
-make run
-```
-
-`make run` shows the application window immediately for local development. A normal installed
-launch continues to start Kastword in the system tray.
-
-No speech model is downloaded or packaged during a normal build. The build needs network access
-only when the pinned Whisper.cpp source is not already available.
-
-To install for the current user:
-
-```sh
make install
```
-This installs the application and desktop launcher below `~/.local`, then refreshes Plasma's
-application database. Launch Kastword from the application menu; `make run` is not needed. Set
-`PREFIX` explicitly to install somewhere else.
-
-To uninstall:
-
-```sh
-make uninstall
-```
-
-Uninstalling removes the installed application files but preserves downloaded models and user
-settings.
-
-The application ID is `io.github.shape_machine.Kastword`; the underscore follows D-Bus guidance
-for the hyphen in the `Shape-Machine` organization name.
-
-### Distribution builds
-
-Distribution packagers can provide the Whisper.cpp dependency without any build-time downloads:
-
-```sh
-cmake -S . -B build -G Ninja \
- -DKASTWORD_FETCH_WHISPER=OFF
-```
-
-This requires a compatible system `whisper` CMake package. Kastword packages must not include a
-speech model; users choose models after installation. The legacy
-`KASTWORD_FETCH_DEFAULT_MODEL=ON` option remains available only for development compatibility.
-
-## Using Kastword
-
-1. Start Kastword. On first run, choose an English-only or multilingual speech model.
-2. Explicitly download a recommended model or select an existing compatible `.bin` file.
-3. Focus the text field or terminal where the result should go.
-4. Press **Meta+Z** (or your configured shortcut) and speak, then press it again.
-5. Kastword transcribes locally and updates the clipboard. If automatic paste is enabled and its
- helper is available, Kastword also sends the configured paste shortcut.
-
-Downloaded models are checksum-verified and stored per user under
-`~/.local/share/kastword/models/`. Speech Models can switch models, show their disk usage, resume or
-retry downloads, and remove managed models. Audio Input can disable microphone use with None, follow
-the system default microphone, or use a specific device. A specific selection is never silently
-replaced when disconnected; dictation remains disabled until that device returns or another input is
-chosen. Settings controls the transcription language, recording limit, automatic-paste behavior,
-paste shortcuts, and global keyboard shortcut. Dictation remains disabled whenever no valid model or
-audio input is available.
-
-Click the tray icon to open or hide the Kastword window. The tray menu can start or stop
-dictation and quit the application.
-
-## Privacy and security model
-
-- Microphone samples live in process memory only until transcription completes.
-- Recordings stop at the configured duration limit (five minutes by default) or a 256 MiB raw
- audio ceiling, whichever comes first.
-- Raw audio and transcription history are not written to disk.
-- Transcribed text is placed on the desktop clipboard and primary selection.
-- The selected model remains in per-user local storage.
-- No telemetry is included. At runtime, network access is used only after an explicit model-download
- action.
-- Model downloads use immutable HTTPS URLs and are activated only after size, format, and SHA-256
- verification. Transcription remains offline.
-- Automatic Wayland paste relies on the separately installed `ydotool`/`ydotoold` service.
-- Custom model files are trusted input parsed inside Kastword; use models from sources you trust.
-- Paste helpers are resolved from the inherited `PATH`; ensure every directory in `PATH` is
- trusted. Kastword refuses to run with elevated privileges.
-
-Clipboard managers, target applications, desktop services, crash dumps, and the operating system
-may retain data independently of Kastword. Review their settings when dictating sensitive text.
-The clear-transcription action clears Kastword's retained text and any matching current clipboard
-or primary selection; it does not erase entries already retained by clipboard-manager history.
-Automatic paste is disabled by default. X11 focus is checked again before keys are sent; Wayland
-does not expose an equivalent global focus check, so focus can change during the short delay.
+Launch Kastword from the application menu. Detailed build, run, packaging, uninstall, and
+contributor instructions are in the [development guide](docs/DEVELOPMENT.md).
## Known limitations
- Full-size Large models require substantial disk space and memory.
-- Paste reliability depends on the session, helper, and target application.
-- `ydotool` requires privileged input-device access configured outside Kastword.
+- Paste reliability depends on the desktop session, helper, focus, and target application.
+- Plasma Wayland automatic paste requires privileged input-device access configured outside
+ Kastword.
+- Plasma X11 and environments outside the tested Plasma setup need broader community testing.
- There are no supported binary packages or release builds yet.
-## Architecture
-
-```text
-Global shortcut / tray
- │
- ▼
- AppController state machine
- ├── AudioCapture ─── Qt Multimedia
- ├── WhisperEngine ── worker thread / whisper.cpp
- ├── ModelManager ─── explicit verified downloads + per-user storage
- └── TextOutput ───── clipboard + optional paste helper
-```
-
-Inference runs away from the UI thread. Recording and transcription status is reported through the
-application window, system tray, and desktop notifications without opening the main window during a
-normal dictation.
-
-## Development and validation
-
-```sh
-make test
-make coverage BUILD_DIR=build-coverage CMAKE_ARGS=-DKASTWORD_FETCH_DEFAULT_MODEL=OFF
-make screenshots
-make lint
-make install-smoke
-make format
-make validate
-```
-
-`make test` builds and runs the deterministic test suites. `make coverage` enables instrumentation,
-enforces the repository's line and branch thresholds, and writes a browsable report to
-`build-coverage/coverage/index.html`, a text summary, and Cobertura XML. It uses an installed
-`gcovr`, or runs it through `uvx` when available. The default gates require at least 68% line and
-55% branch coverage and can be raised explicitly with `COVERAGE_MIN_LINE` and
-`COVERAGE_MIN_BRANCH`. `make lint` checks C++ formatting without changing files, while `make format`
-applies it.
-`make screenshots` renders the four top-level application views in a 760×520 logical window with
-deterministic fake data and safely replaces the PNG files in `screenshots/`. It requires an
-active graphical session so the output inherits the current Plasma theme, icons, fonts, and display
-scale. It does not access the microphone or network and preserves the previous complete set if
-generation fails. Generated screenshots are covered by the repository-level REUSE annotation in
-`REUSE.toml`.
-`make install-smoke` installs into a temporary prefix, resolves and executes the application through
-the installed desktop entry, validates the metadata, and verifies that uninstall removes every
-installed file. `make validate` runs the build, tests, formatting check, REUSE license validation,
-desktop metadata validation, and QML linting. QML linting uses Qt's CMake target, so it follows the
-configured Qt toolchain and `BUILD_DIR` instead of assuming a distribution-specific executable
-path. Coverage and license checks use installed `gcovr` and `reuse` commands, or fetch temporary
-tools through `uvx`. The remaining checks require `appstreamcli`, `desktop-file-validate`, and
-`clang-format`. CI invokes the same Make targets without downloading the model, installs packages
-from a dated Arch Linux Archive snapshot, and also runs the tests under AddressSanitizer and
-UndefinedBehaviorSanitizer. Every CI run publishes the exact coverage summary on its job page and
-uploads the complete HTML, text, and XML reports as the `coverage-report` artifact.
-
-Kate users can open the repository directory and enable the Project, Build, and LSP Client
-plugins. `.kateproject` provides Build, Run, Test, and Clean targets, while CMake generates
-`build/compile_commands.json` for clangd.
-
-## Roadmap
-
-- broader Plasma Wayland/X11 and application compatibility testing
-- broader audio-device and hot-plug compatibility testing
-- reproducible distribution packages and signed binary releases
-
-Roadmap items are intentions, not promised dates.
+## Help and project information
-## License
+- [Automatic paste setup and troubleshooting](docs/AUTOMATIC_PASTE.md)
+- [Source installation and development](docs/DEVELOPMENT.md)
+- [Report a bug or request a feature](https://github.com/Shape-Machine/Kastword/issues)
+- [License](LICENSES/GPL-3.0-or-later.txt)
-Kastword is licensed under `GPL-3.0-or-later`. See `LICENSES/GPL-3.0-or-later.txt`.
-AppStream metadata is provided under `CC0-1.0`. Third-party components retain their own licenses.
+Kastword is licensed under `GPL-3.0-or-later`. AppStream metadata is provided under `CC0-1.0`.
+Third-party components retain their own licenses.
diff --git a/docs/AUTOMATIC_PASTE.md b/docs/AUTOMATIC_PASTE.md
new file mode 100644
index 0000000..1467e4c
--- /dev/null
+++ b/docs/AUTOMATIC_PASTE.md
@@ -0,0 +1,82 @@
+
+
+# Automatic paste setup and troubleshooting
+
+Kastword always places completed transcription on the clipboard. Automatic paste is optional,
+disabled by default, and uses a separately installed helper to send your configured paste shortcut.
+If no helper is available, copy and transcription still work for manual pasting.
+
+## Install the session helper
+
+On CachyOS or Arch Linux:
+
+```sh
+# Plasma Wayland
+sudo pacman -S --needed ydotool
+
+# Plasma X11
+sudo pacman -S --needed xdotool
+```
+
+X11 paste uses `xdotool`. Kastword checks that the focused window is unchanged immediately before
+sending keys, although focus can still change afterward.
+
+Wayland does not expose an equivalent global focus check. Kastword uses `ydotool`, whose
+`ydotoold` service creates a virtual keyboard through `/dev/uinput`. Focus can change during the
+short delay before keys are sent.
+
+## Enable automatic paste on Plasma Wayland
+
+Enable and start the packaged user service:
+
+```sh
+systemctl --user enable --now ydotool.service
+systemctl --user is-active ydotool.service
+```
+
+The second command must print `active`. Verify that the input device and daemon socket exist and
+that the client can connect:
+
+```sh
+ls -l /dev/uinput "$XDG_RUNTIME_DIR/.ydotool_socket"
+ydotool debug
+```
+
+Test actual input delivery by running this command, immediately focusing an editable field, and
+waiting one second. `Kastword ydotool test` should appear:
+
+```sh
+sleep 1 && ydotool type 'Kastword ydotool test'
+```
+
+## Troubleshoot `ydotoold`
+
+If the service is inactive or the socket is missing, inspect its status and current-boot log:
+
+```sh
+systemctl --user status ydotool.service
+journalctl --user -u ydotool.service -b
+```
+
+Errors mentioning `/dev/uinput`, the daemon socket, or permission denied mean the helper is not
+usable by the logged-in user. Check that the distribution package is current, restart the user
+service, and log out and back in after changing device or group permissions.
+
+Kastword never requests elevated permissions and does not start or configure `ydotoold`. Do not run
+Kastword or `ydotool` with `sudo`.
+
+## Security and privacy considerations
+
+- Paste helpers are resolved from the inherited `PATH`; ensure every directory in `PATH` is trusted.
+- The target application receives the transcription and may retain it.
+- Clipboard managers may save clipboard history independently of Kastword.
+- The clear-transcription action clears matching current clipboard selections, not entries already
+ retained by a clipboard manager.
+- Automatic paste sends keystrokes to whichever field has focus at delivery time. Confirm focus
+ before dictating sensitive text, especially on Wayland.
+
+Return to the [Kastword overview](../README.md) or see the [source installation and development
+guide](DEVELOPMENT.md).
diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md
new file mode 100644
index 0000000..3012d88
--- /dev/null
+++ b/docs/DEVELOPMENT.md
@@ -0,0 +1,143 @@
+
+
+# Source installation and development
+
+Kastword is early alpha software and currently has no supported binary releases. This guide covers
+source installation, packaging, architecture, and contributor tooling. See the [project
+overview](../README.md) for product capabilities, privacy, and usage.
+
+## Dependencies
+
+On CachyOS or Arch Linux:
+
+```sh
+sudo pacman -S --needed \
+ base-devel cmake ninja git \
+ qt6-base qt6-declarative qt6-multimedia \
+ extra-cmake-modules kirigami kdeclarative \
+ kconfig kcoreaddons kdbusaddons kglobalaccel ki18n \
+ knotifications kstatusnotifieritem
+```
+
+Automatic paste requires a separate session helper. See [Automatic paste setup and
+troubleshooting](AUTOMATIC_PASTE.md).
+
+## Build and run
+
+The first default build downloads one immutable, pinned dependency:
+
+- `whisper.cpp` source at commit `a91dd3be72f70dd1b3cb6e252f35fa17b93f596c`
+
+```sh
+make
+make run
+```
+
+`make run` shows the application window immediately for local development. A normal installed
+launch starts Kastword in the system tray. No speech model is downloaded or packaged during a
+normal build. The build needs network access only when the pinned Whisper.cpp source is not already
+available.
+
+## Install and uninstall
+
+Install for the current user:
+
+```sh
+make install
+```
+
+This installs below `~/.local` and refreshes Plasma's application database. Set `PREFIX` explicitly
+to install elsewhere. Launch Kastword from the application menu after installation.
+
+Uninstall application-owned files:
+
+```sh
+make uninstall
+```
+
+Uninstalling preserves downloaded models and user settings.
+
+The application ID is `io.github.shape_machine.Kastword`; the underscore follows D-Bus guidance
+for the hyphen in the `Shape-Machine` organization name.
+
+## Distribution builds
+
+Packagers can provide Whisper.cpp without a build-time download:
+
+```sh
+cmake -S . -B build -G Ninja \
+ -DKASTWORD_FETCH_WHISPER=OFF
+```
+
+This requires a compatible system `whisper` CMake package. Kastword packages must not include a
+speech model; users choose models after installation. The legacy
+`KASTWORD_FETCH_DEFAULT_MODEL=ON` option remains available only for development compatibility.
+
+## Architecture
+
+```text
+Global shortcut / tray
+ │
+ ▼
+ AppController state machine
+ ├── AudioCapture ─── Qt Multimedia
+ ├── WhisperEngine ── worker thread / whisper.cpp
+ ├── ModelManager ─── explicit verified downloads + per-user storage
+ └── TextOutput ───── clipboard + optional paste helper
+```
+
+Inference runs away from the UI thread. Recording and transcription status is reported through the
+application window, system tray, and desktop notifications without opening the main window during
+a normal dictation.
+
+## Development and validation
+
+```sh
+make test
+make coverage BUILD_DIR=build-coverage CMAKE_ARGS=-DKASTWORD_FETCH_DEFAULT_MODEL=OFF
+make screenshots
+make lint
+make install-smoke
+make format
+make validate
+```
+
+`make test` builds and runs the deterministic test suites. `make coverage` enables instrumentation,
+enforces the repository's line and branch thresholds, and writes HTML, text, and Cobertura XML
+reports under `build-coverage/coverage/`. The default gates require at least 68% line and 55% branch
+coverage and can be raised with `COVERAGE_MIN_LINE` and `COVERAGE_MIN_BRANCH`. Coverage uses an
+installed `gcovr`, or runs it through `uvx` when available.
+
+`make screenshots` renders all four top-level views in a 760×520 logical window with deterministic
+fake data and safely replaces the permanent PNG assets in `screenshots/`. It requires an active
+graphical session so output inherits the current Plasma theme, icons, fonts, and display scale. It
+does not access the microphone or network and preserves the previous complete set on failure.
+
+`make lint` checks C++ formatting without changing files; `make format` applies it.
+`make install-smoke` installs into a temporary prefix, launches through the installed desktop entry,
+validates metadata, and verifies complete uninstall behavior. `make validate` runs the build, tests,
+formatting check, REUSE validation, desktop metadata validation, and QML linting. Coverage and
+license checks use installed `gcovr` and `reuse`, or temporary tools through `uvx`. Remaining checks
+require `appstreamcli`, `desktop-file-validate`, and `clang-format`.
+
+CI uses the same Make targets without downloading a model, installs packages from a dated Arch
+Linux Archive snapshot, and runs tests under AddressSanitizer and UndefinedBehaviorSanitizer. Each
+run publishes its coverage summary and uploads the HTML, text, and XML reports as the
+`coverage-report` artifact.
+
+## Kate
+
+Open the repository directory and enable the Project, Build, and LSP Client plugins.
+`.kateproject` provides Build, Run, Test, and Clean targets, while CMake generates
+`build/compile_commands.json` for clangd.
+
+## Roadmap
+
+- broader Plasma Wayland/X11 and application compatibility testing
+- broader audio-device and hot-plug compatibility testing
+- reproducible distribution packages and signed binary releases
+
+Roadmap items are intentions, not promised dates.
diff --git a/screenshots/01-offline-dictation.png b/screenshots/01-offline-dictation.png
index 4d2dc84..f0667bf 100644
Binary files a/screenshots/01-offline-dictation.png and b/screenshots/01-offline-dictation.png differ
diff --git a/screenshots/02-speech-models.png b/screenshots/02-speech-models.png
index d62102a..0508807 100644
Binary files a/screenshots/02-speech-models.png and b/screenshots/02-speech-models.png differ
diff --git a/screenshots/03-audio-input.png b/screenshots/03-audio-input.png
index 18efa57..b4f1dcb 100644
Binary files a/screenshots/03-audio-input.png and b/screenshots/03-audio-input.png differ
diff --git a/screenshots/04-settings.png b/screenshots/04-settings.png
index cbb285a..46ec503 100644
Binary files a/screenshots/04-settings.png and b/screenshots/04-settings.png differ
diff --git a/tests/ModelManagerTest.cpp b/tests/ModelManagerTest.cpp
index 221bf4b..4c8da59 100644
--- a/tests/ModelManagerTest.cpp
+++ b/tests/ModelManagerTest.cpp
@@ -544,7 +544,7 @@ void ModelManagerTest::invalidatesChangedManagedModel() {
QTRY_VERIFY(!manager.modelReady());
QTRY_VERIFY(!manager.busy());
- QVERIFY(!QFileInfo::exists(path));
+ QTRY_VERIFY(!QFileInfo::exists(path));
}
void ModelManagerTest::invalidatesChangedLocalModel() {
diff --git a/tools/check-documentation-links.sh b/tools/check-documentation-links.sh
new file mode 100755
index 0000000..6bd79ae
--- /dev/null
+++ b/tools/check-documentation-links.sh
@@ -0,0 +1,74 @@
+#!/bin/sh
+# SPDX-FileCopyrightText: 2026 Sri Rang
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+set -eu
+
+if [ "$#" -ne 1 ] || [ ! -d "$1" ]; then
+ echo "Usage: $0 DOCUMENTATION_ROOT" >&2
+ exit 2
+fi
+
+documentation_root=$(CDPATH= cd -- "$1" && pwd -P)
+documents=$(mktemp)
+targets=$(mktemp)
+trap 'cmake -E rm -f "$documents" "$targets"' EXIT HUP INT TERM
+
+if [ -f "$documentation_root/README.md" ]; then
+ printf '%s\n' "$documentation_root/README.md" > "$documents"
+fi
+if [ -d "$documentation_root/docs" ]; then
+ find "$documentation_root/docs" -type f -name '*.md' -print >> "$documents"
+fi
+
+while IFS= read -r document; do
+ awk -v document="$document" '
+ { text = text " " $0 }
+ END {
+ markdown = text
+ html = text
+ while (match(markdown, /\]\([^)]*\)/)) {
+ print document "\t" substr(markdown, RSTART + 2, RLENGTH - 3)
+ markdown = substr(markdown, RSTART + RLENGTH)
+ }
+ while (match(html, /src="[^"]+"/)) {
+ print document "\t" substr(html, RSTART + 5, RLENGTH - 6)
+ html = substr(html, RSTART + RLENGTH)
+ }
+ }
+ ' "$document"
+done < "$documents" > "$targets"
+
+while IFS=" " read -r document destination; do
+ case "$destination" in
+ ''|'#'*|http://*|https://*|mailto:*) continue ;;
+ esac
+
+ relative_path=${destination%%#*}
+ fragment=
+ if [ "$relative_path" != "$destination" ]; then
+ fragment=${destination#*#}
+ fi
+ resolved_directory=$(CDPATH= cd -- "$(dirname -- "$document")" && pwd -P)
+ resolved_path="$resolved_directory/$relative_path"
+ if [ ! -f "$resolved_path" ]; then
+ echo "$document: broken relative link: $destination" >&2
+ exit 1
+ fi
+
+ if [ -n "$fragment" ] && ! awk -v wanted="$fragment" '
+ /^#{1,6}[[:space:]]/ {
+ heading = $0
+ sub(/^#+[[:space:]]+/, "", heading)
+ heading = tolower(heading)
+ gsub(/[^a-z0-9 _-]/, "", heading)
+ gsub(/[ _]+/, "-", heading)
+ if (heading == wanted)
+ found = 1
+ }
+ END { exit !found }
+ ' "$resolved_path"; then
+ echo "$document: missing heading for link: $destination" >&2
+ exit 1
+ fi
+done < "$targets"