# Contributing to Kartend
Thank you for considering contributing to Kartend! This document outlines the process for contributing.
## Development Setup
1. Install dependencies (see [docs/dev/building.md](docs/dev/building.md)):
- Qt6 (Core, Gui, Widgets, Sql, Concurrent, Multimedia, MultimediaWidgets)
- CMake 3.20+
- C++23 compiler (Clang or GCC)
2. Build:
```bash
.scripts/build.sh --debug
```
3. Run tests:
```bash
.scripts/build.sh --tests --run-tests
```
## Submitting Changes
1. Fork the repository and create a feature branch from `main`. Use one
branch per logical change, branched fresh off `main` — don't stack a new
change onto a prior, still-unmerged branch.
2. Make your changes. Follow the existing code style (enforced by
`.clang-format` and `.clang-tidy`) and the architecture rules below.
3. Add tests for new functionality where applicable. Tests that exercise
migrations or real SQL paths **must not mock the database** — use the real
on-disk SQLite via the integration harness (see
[docs/dev/testing.md](docs/dev/testing.md)).
4. Run the maintenance gate before pushing:
```bash
.scripts/build.sh --maintenance # python guardrails, clang-format, clang-tidy, IWYU, cppcheck
```
The gate opens with the **Python guardrail lints** (the `check-*.py`
scripts: module layering + DI invariants, `QTimer::singleShot` "why"
comments, src↔tests mapping, bd-ID leakage, required-checks lockstep) —
the same set CI's `script-lint` job enforces; findings are **fatal** and
fail in under a second, before the build starts. **clang-format drift is
fatal** too, and the formatter is pinned to **clang-format-19** — a newer
system clang-format reformats differently and fails CI. To format just what
you touched, run `.scripts/format.sh` (or `--check`); it finds the pinned
binary or falls back to the `kartend-ci` container correctly, which a
hand-written `docker run` does not — see
[docs/dev/building.md](docs/dev/building.md). clang-tidy /
cppcheck / raw-`qDebug` findings surface as non-fatal notices. Local Qt is
newer than CI's pinned **Qt 6.4.2**, so reproduce CI failures in the
pinned image with `.scripts/ci-local.sh` (see
[docs/dev/ci-local.md](docs/dev/ci-local.md)).
5. Open a pull request with a clear description of the changes.
> **Automated agents** (Copilot, Claude Code, Codex, …) working in this repo
> must get explicit maintainer approval before each `git commit` and
> `git push`, and must not pass `--no-verify` / `--no-gpg-sign` unless asked.
### Quarterly lint review
The `.clang-tidy` file disables a number of `modernize-*` / `readability-*` /
`misc-*` checks that were too noisy at one point in the project's history.
Each disabled check carries an inline rationale comment, but the codebase
moves fast and what was unworkable six months ago may be tractable now.
Maintainers: scan the disabled-check list each quarter and try re-enabling
one or two that look ripe. Even partial wins (a handful of cleanups followed
by re-enabling a check) compound over time.
The same cadence applies to **sanitizer suppressions**. The TSan and LSan
lists live in [`tests/suppressions/tsan.txt`](tests/suppressions/tsan.txt) and
[`tests/suppressions/lsan.txt`](tests/suppressions/lsan.txt); each entry cites
the bd issue that justified it. These are meant to be *transient* workarounds
for upstream (Qt, glib, PulseAudio, …) races and leaks, but without a review
cadence they accrete forever and become permanent fixtures. Each quarter,
re-evaluate whether a suppressed issue has since been fixed upstream and the
entry can be dropped. The policy and audit notes are in
[docs/dev/sanitizer-suppressions.md](docs/dev/sanitizer-suppressions.md).
**This review is automated** (Kartend-b7nf8). The
[`quarterly-lint-review`](.github/workflows/quarterly-lint-review.yml) workflow
runs on a quarterly cron (1st of Jan/Apr/Jul/Oct) and opens a dated tracking
issue (label `lint-review`) that enumerates the current disabled clang-tidy
checks and every TSan/LSan suppression entry with its cited bd ID — so the
review is a worked checklist, not a reminder you have to remember. You don't
need to scan the files by hand; just work the issue the job files and close it.
Trigger it on demand any time via the workflow's **Run workflow**
(`workflow_dispatch`) button.
### Optional pre-commit hook
An opt-in git pre-commit hook lives in `.scripts/git-hooks/pre-commit`. It
runs `clang-format --dry-run --Werror` on every staged `.cpp`/`.h` file and
exits non-zero if any drift, so a `git commit` fails fast instead of letting
the same drift surface in CI's `--maintenance --format-check`. The hook also
checks version-string consistency across `VERSION`, `PKGBUILD`, the ebuild
filename, and the AppStream metainfo so a version bump can't be partial.
Install with:
```
.scripts/git-hooks/install.sh
```
If the hook ever needs to be bypassed for an emergency commit, use
`git commit --no-verify` — but please don't habituate; the same checks run
in CI and will catch the drift there.
## Code Style
- 2-space indentation, 100 column limit
- Implicit boolean null checks (`if (!ptr)` not `if (ptr == nullptr)`)
- All `QTimer::singleShot` calls must have a comment explaining the delay
- UI constants go in the topical `src/utils/uiconstants/.h` subheader (e.g. `grid.h`, `dialog.h`, `icons.h`) — no magic numbers
- Use `[[nodiscard]]` on const getters and factory functions
- See [docs/dev/architecture.md](docs/dev/architecture.md), [docs/dev/building.md](docs/dev/building.md), and the rules baked into `.clang-format` / `.clang-tidy` for full conventions
- Debugging: [docs/dev/dev-debugging.md](docs/dev/dev-debugging.md) covers logging-category filters (`lcPerfTrace`, `lcSearchDiag`, …), ASan / TSan / UBSan triage, attaching GDB/LLDB, Valgrind/heaptrack recipes, the Qt-specific crash patterns that come up most, and the rationale for the `QTimer::singleShot` "why" comment rule.
## Architecture
See [docs/dev/architecture.md](docs/dev/architecture.md) for module hierarchy, signal
flow, and ownership model. A few rules are load-bearing and enforced:
- **Layering DAG.** Module dependencies must follow the directed acyclic graph
enforced by `.scripts/check-layering.py` — run as part of `--maintenance`
(the python-guardrails step) **and** in CI's `script-lint` job.
Introducing an edge that violates it fails the build. See
[docs/dev/layering.md](docs/dev/layering.md).
- **`ApplicationContext` (ctx) access; no sibling-manager pointers in setup
structs.** A `*Setup` struct carries `const ApplicationContext *ctx` plus
only non-manager refs (widgets, value containers, callbacks); sibling
manager/service pointers are read through `ctx` so a manager can't pin its
siblings' lifetimes. `check-layering.py` fails the build if a `*Manager *` /
`*Service *` field is added to a `*Setup` struct — again both locally in
`--maintenance` and in CI `script-lint`. See
[docs/dev/layering.md](docs/dev/layering.md).
- **`parent()` is a runtime lifetime guard, not just ownership metadata.**
Some call sites check `parent()` to decide whether an object is still
attached to its QObject tree before touching siblings. Before changing a
constructor's parent (e.g. `this` → `nullptr`), `grep -rn "parent()"
src/` and confirm no live path depends on it. See
[docs/dev/architecture.md](docs/dev/architecture.md) (§ QObject lifecycle).
## Issue tracking conventions (beads / `bd`)
Kartend uses [beads](https://github.com/steveyegge/beads) for internal task
tracking. Most contributors don't need to interact with it directly — GitHub
issues remain the public-facing entry point for bug reports and feature
requests (see below). For PRs that touch the codebase, the convention is:
**Beads IDs (`Kartend-XXXX`) may appear in:** code comments, commit messages
and PR descriptions, `CHANGELOG.md`, and contributor-facing dev docs
under `docs/` (architecture, layering, testing, building, …). The IDs serve
as cross-references to historical extraction or refactor work.
**Beads IDs must NOT appear in:** user-facing string literals (tooltips,
labels, dialogs, error messages), `readme.md`, AppStream metadata under
`packaging/`, or the wiki content under `docs/user/*.md`. End users see
those surfaces and a `Kartend-XXXX` reference there is noise to them.
**The `.beads/` database is not committed.** It's gitignored, so a fresh
clone has no way to resolve a `Kartend-XXXX` reference back to its issue —
`bd show Kartend-XXXX` only works for maintainers who carry the local
database. Treat the IDs as opaque provenance markers, not links: every commit
message, PR description, and code comment that cites one is written to stand
on its own, so you are never expected to look an ID up to understand a change.
Maintainers can still resolve them through the bd/Dolt sync when deeper
archaeology is needed.
## Reporting Issues
Use the GitHub issue templates for bugs and feature requests.
## Translations
All user-visible strings are wrapped in `tr()` and extracted into Qt
Linguist `.ts` files under `translations/`. The baseline locale is
`translations/kartend_en.ts`; `lrelease` runs as part of the regular build
and embeds compiled `.qm` files into the binary under `:/i18n/`. `main.cpp`
loads the locale-matching `.qm` at startup, falling back to source-text
English when no match ships.
To refresh the source-text database after adding or changing `tr()` calls:
```bash
cmake --build build --target update_translations
```
To start a new locale (replace `de` with the target ISO code):
```bash
cp translations/kartend_en.ts translations/kartend_de.ts
```
Edit `kartend_de.ts` in Qt Linguist (`lupdate` / `lrelease` ship with
`qt6-tools`), then submit a PR. CMakeLists.txt globs `translations/kartend_*.ts`
with `CONFIGURE_DEPENDS`, so the new file is picked up automatically on the next
configure — no CMakeLists edit is needed. `qt_add_lupdate` merges new source
strings into every `.ts` without dropping existing translations, and
`qt_add_lrelease` compiles each non-source locale to a `.qm` at build time.
## Cutting a Release
Run `.scripts/bump-version.sh ` — it updates every synced file in
one shot and is the only supported way to prep a release. Do **not** bump
the files by hand from this list; the list exists so reviewers know what
the script touches, and it has drifted before (the v0.0.13 prep missed
`vcpkg.json` by following an older copy of this section).
Before pushing a `v` tag, every file below must agree on the new
version. The release workflow (`.github/workflows/release.yml`) fails the
tag build if any are out of sync.
1. `VERSION` — single source of truth; CMake reads this at configure time.
2. `packaging/PKGBUILD` — `pkgver=`.
3. `packaging/kartend-.ebuild` — the ebuild file must be renamed
(or copied) to match the new version.
4. `packaging/io.github.EtherAura.Kartend.metainfo.xml` — add a new
`` entry **with a
`` block** at the top of ``. The CI check only
compares the most-recent `` version; a missing description
passes CI but ships a release-notes-less entry to GNOME Software,
KDE Discover, and Flathub. (The script inserts a bare entry — fill in
the description before tagging.)
5. `vcpkg.json` — `version-string` (Kartend-szdbl keeps the Windows
dependency manifest's version honest).
6. `CHANGELOG.md` — promote `[Unreleased]` to `[X.Y.Z] - YYYY-MM-DD` and
open a fresh `[Unreleased]` section (manual — the script does not
edit the changelog).
After tagging, the release workflow updates `PKGBUILD` `sha256sums=` on
`main` automatically — do not pre-commit a placeholder hash.
### Changelog archiving (cadence)
The root `CHANGELOG.md` keeps `[Unreleased]` plus the **three most recent
releases**; everything older lives in `docs/changelogs/v0.0.x.md`. Promoting
`[Unreleased]` in step 6 leaves four release sections in the root, so the same
release moves the now-oldest one into the archive: cut its `## [X.Y.Z]` block
(and its comparison link-ref) into `docs/changelogs/`, then refresh the root's
bottom-of-file link-refs and the "Older releases" pointer. This keeps the root
file small enough to grep and to render cleanly in PR diffs (Kartend-zdgj).
### Opting out of the CHANGELOG Enforcer
The **CHANGELOG Enforcer** workflow fails any change touching `src/` (outside
`tests/`) that adds no entry under `[Unreleased]`. Doc-only, test-only and
CI-only changes are exempt automatically. For a genuinely internal change —
a refactor, a diagnostic — opt out explicitly:
- **On a PR:** add the `no-changelog` label.
- **On a push:** put `[no-changelog]` **on a line of its own** in the commit
message. Mentioning it mid-sentence does not count, so a commit that merely
discusses the marker is still checked. Neither does a subject line that
merely ends in it — many older subjects do, and they passed only because
something else in the same push added a CHANGELOG entry. Keep the suffix
if you like it there, but also give the marker its own line in the body.
The marker exempts **only the commit carrying it**. Batching a marked commit
with unmarked `src/` work does not exempt that work — each commit stands on
its own, and a push mixing the two is still checked for the unmarked part.