Covers the build/test/lint commands (including running one test function and a throwaway --db database), the CLI/GUI split, database location resolution, the setStage rule, the stage vocabulary, the lane -based Sankey layout and its geometric test, and the data-driven edit form. REUSE.toml annotates it as CC0 like the other docs. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01BxDf7HqD1xPnsZP8wt3NTk
7.7 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
What this is
Kareer is a KDE-style job application tracker: C++20 backend, QML/Kirigami frontend, ECM/CMake build, Qt 6 + KDE Frameworks 6. Data is a local SQLite file. Every GUI action is also a kareer CLI subcommand so scripts can drive it headlessly. Licensed GPL-3.0-or-later, REUSE-compliant.
Commands
# Configure + build (Debug, tests on). A configured build/ dir may already exist.
cmake --preset ninja-dev # or: cmake -B build -G Ninja
cmake --build build
./build/bin/kareer # GUI
./build/bin/kareer list --json # any recognized subcommand runs headless
# Tests (QtTest binaries; appstreamtest is added automatically by KDECMakeSettings)
ctest --test-dir build --output-on-failure
ctest --test-dir build -R sankeylayouttest # one test binary
./build/bin/sankeylayouttest layoutReflectsTransitionCounts # one test function
# Point the app at a throwaway DB (GUI or any subcommand; file created if missing)
./build/bin/kareer --db build/dev.sqlite add --company Acme --title Dev
./build/bin/kareer --db build/dev.sqlite
# Autotests use the env var instead: KAREER_DB_PATH=/tmp/x.sqlite
# Formatting: CI runs clang-format (pinned major 22) with the committed .clang-format
find src autotests -name '*.h' -o -name '*.cpp' | xargs clang-format --dry-run --Werror
# REUSE/SPDX check (CI): reuse lint
Configuring installs a clang-format git pre-commit hook via ECM if clang-format is on PATH. CI builds and tests inside the KDE Flatpak SDK (flatpak-builder with run-tests: true in the manifest), not a bare CMake build.
Architecture
src/ is intentionally flat: one class per concern, no subfolders. Backend classes are exposed to QML with QML_ELEMENT via the io.github.toservetheking.Kareer QML module declared in src/CMakeLists.txt. QML files are meant to be thin renderers; logic lives in C++ so it can be unit-tested.
Entry point and CLI/GUI split. main.cpp checks whether argv[1] is a known subcommand (Cli::isSubcommand). If so it builds a QCoreApplication and hands off to Cli::run in clicommands.cpp; otherwise it starts the Kirigami GUI. Adding a subcommand means adding it to both the list in isSubcommand and the dispatch in run, plus the README usage block.
Database location. JobsDatabase::defaultPath() resolves --db (stripped from argv in main.cpp before CLI/GUI routing) > KAREER_DB_PATH > the path the user chose in the GUI (kareerrc [Database] Path, loaded at startup for both GUI and CLI by DatabaseLocation::loadConfiguredPath) > $XDG_DATA_HOME/kareer/kareer.sqlite. On a GUI first run (nothing forced, file missing) JobsDatabase::setSelectionPending(true) makes default-constructed instances open nothing until DatabaseSetupDialog.qml picks a location. DatabaseLocation (QML singleton) writes the choice and emits changed; Main.qml then refreshes JobsModel, and every model calls m_db.reopenIfPathChanged() at the start of its refresh, so switching databases needs no restart.
Persistence. JobsDatabase is the only class that touches SQLite. Two tables: jobs and stage_history. Schema is created with CREATE TABLE IF NOT EXISTS in migrate(); there is no version number, so schema changes need an idempotent migration step there. Each JobsDatabase instance opens its own uniquely named QSqlDatabase connection, so JobsModel, StatsModel and SankeyModel each hold their own instance on the same file. Consequence: after a write through one model, the others do not know; QML wires this up (DashboardPage listens to JobsModel.countChanged and calls statsModel.refresh() and the Sankey refresh()).
Stage changes must go through setStage. updateJob deliberately does not write the stage column. addJob records a synthetic Start -> stage transition and setStage records every move, and that history is the sole input to the Sankey diagram. Bypassing setStage silently corrupts the pipeline view. The same applies to the GUI: JobEditModel::save() calls updateJob and then setStage when the stage combo changed (it once skipped the latter, silently dropping stage edits).
Stage vocabulary. JobStage (jobstage.h) is a closed, fixed list (Applied, Screening, Interview, Onsite, Offer, Accepted, Rejected, Withdrawn, Ghosted, plus the synthetic Start). It also owns each stage's Sankey column, in-column stacking order, color, and terminal flag. Input is canonicalized case-insensitively on write. Adding a stage touches this file, the migration's canonicalization loop, and the README.
Sankey layout. SankeyModel::reload() turns stage_history into one left-to-right path per job (Start, the funnel stages it reached in increasing column order, then its current stage if terminal), so backward/sideways moves never become ribbons and node values equal the number of jobs that reached them. relayout() computes all geometry, including SVG path strings for QtQuick.Shapes PathSvg; SankeyDiagram.qml only draws the nodes/links lists. Layout is lane-based: links to Rejected/Withdrawn/Ghosted are drop-offs that travel in under-lanes below each column's node; main-line links that skip a column travel in over-lanes above it; each column stack is top-aligned and the whole block is centered. Every ribbon crossing a given gap between columns uses the same x endpoints, so ribbons can only cross if their vertical order differs at the two ends of a gap; the lane/slot orderings are chosen to keep it equal everywhere except the final gap into the outcome nodes. reload() re-reads the DB; relayout() recomputes geometry from cached counts (used on resize via a debounce timer). autotests/sankeylayouttest.cpp checks this on the drawn geometry (it parses pathData and asserts ribbons never overlap) and points KAREER_DB_PATH at a temp file because SankeyModel always opens the default path.
Edit form. The add/edit form is data-driven rather than hand-written per field: JobFieldCatalog is the static list of categories and fields (id, label, row type, combo options, spin range); JobEditModel is a QAbstractListModel with one row per field holding the current values, and ApplicationEditPage.qml renders it with a DelegateChooser on rowType. Field ids must match the keys JobsModel::mapFromJob / jobFromMap use. Adding a form field means: Job struct, JobsDatabase columns and jobFromQuery, JobsModel roles and map conversion, JobFieldCatalog, and the CLI options.
Window layout. Main.qml uses a two-column pageStack: ApplicationsPage (sidebar list) is fixed, and the second column is swapped between DashboardPage and ApplicationEditPage with pageStack.replace. A single JobsModel instance is created in Main.qml and passed down as a property.
Conventions
- Every source file starts with an SPDX header (
GPL-3.0-or-laterfor code; docs like README/CONTRIBUTING are CC0). Files that cannot carry one are listed inREUSE.toml. CI fails on missing headers. - Qt string style:
using namespace Qt::Literals::StringLiterals;withu"..."_s; user-visible strings go throughi18n/i18nc. main.cppinstalls a message handler that drops a few known-benign Qt/Kirigami warnings; add toisBenignFrameworkNoiserather than silencing categories wholesale.- Releasing: bump
project(... VERSION ...)inCMakeLists.txt, add a<release>entry to the metainfo XML, updatepkgver/checksum indist/arch/PKGBUILD, then push av*tag;build.ymlbuilds, signs and publishes the Flatpak. The Flatpak manifest'ssourcesblock is rewritten by CI to build from the checkout. - PRs use
.github/PULL_REQUEST_TEMPLATE.md: reason for change, test plan, screenshots for UI changes.