16 KiB
AGENTS.md
C++ desktop implementation (originally based on a Tauri Rust+TS version) — single wxWidgets binary built with CMake + FetchContent.
Project structure
core/—ccm_corestatic library. UI-agnostic domain, ports, services, infra adapters. Never depends on wxWidgets. Seecore/AGENTS.md.ui_wx/—ccm_ui_wxstatic library. The only place that touches wxWidgets. Seeui_wx/AGENTS.md. Shipsui_wx/assets/ygo_card_back.pngandui_wx/assets/digibattle99_card_back.png(offline preview fallbacks);app/CMakeLists.txtcopies them to<exeDir>/assets/when linkingccm.app/—ccmexecutable (composition root). Wires concrete adapters into services. Seeapp/AGENTS.md.tests/—ccm_core_testsdoctest binary. Pure-logic tests against in-memory fakes. Seetests/AGENTS.md.docs/— long-form developer documentation. Start withdocs/adding-a-new-game.mdfor the canonical end-to-end procedure for extending the app with a new TCG. Seedocs/AGENTS.md..github/workflows/— GitHub Actions CI/release workflows. See.github/workflows/AGENTS.mdfor orchestrator/reusable workflow rules and CI invariants.cmake/—Toolchain.cmake(Clang first, MinGW-w64 fallback),Dependencies.cmake(FetchContent pins),CompilerWarnings.cmake(ccm_warningsinterface target).CMakeLists.txt— top-level. Defines optionsCCM_USE_SYSTEM_WX(default OFF) andCCM_BUILD_TESTS(default ON).- Build metadata option:
CCM_APP_VERSION(defaults to${PROJECT_VERSION} (localbuild)for local/manual builds, overridden by CI).
- Build metadata option:
Architecture rules (do not break)
- Dependencies point inward only:
app->ui_wx->core.coredepends on no other first-party target. coremust not include any wx header. CI-equivalent:rg "wx/" core/must return zero hits.- Cross-boundary types are domain types and
ccm::ui::AppContext. UI code consumes services via the references inAppContext— never by including a concrete adapter header. - Errors cross port boundaries as
ccm::Result<T, E=std::string>(seecore/include/ccm/util/Result.hpp). Do not throw across ports; reserve exceptions for genuinely unrecoverable bugs. - JSON layout must stay byte-for-byte stable: aliases
releaseDate,setNo,firstEdition,dataStorage,defaultGame, and thesignedJSON key (mapped to C++ fieldsigned_). If you touch a domain type, update the round-trip test intests/domain_json_tests.cpp.
Toolchain
- Compilers: Clang 14+ preferred, MinGW-w64 GCC 11+ fallback on Windows. Do not add MSVC support.
- C++ standard: C++20 (
CMAKE_CXX_STANDARD 20,CXX_EXTENSIONS OFF). - Build system: CMake 3.22+ with
FetchContent. Pin every dep by tag incmake/Dependencies.cmake; never usemaster.
Key dependencies
| Library | Version | Purpose |
|---|---|---|
| nlohmann/json | v3.11.3 | All JSON serde |
| libcpr/cpr | 1.10.5 | HTTPS (libcurl built in-tree, Schannel on Windows) |
| wxWidgets | v3.2.5 | UI toolkit (only ui_wx/ may use it) |
| doctest | v2.4.11 | Tests (only when CCM_BUILD_TESTS=ON) |
Commands
Run from the workspace root.
- Configure (Clang/Ninja, FetchContent wx):
cmake -S . -B build -G Ninja - Configure (Windows MinGW-w64 fallback — verified working with MSYS2 UCRT64 GCC 15.2 + CMake 4.x):
cmake -S . -B build -G "MinGW Makefiles" -DCMAKE_BUILD_TYPE=Release - Configure with system wx for fast iteration:
cmake -S . -B build -G Ninja -DCCM_USE_SYSTEM_WX=ON - Build everything:
cmake --build build --parallel - Run the app:
./build/bin/ccm3(.\build\bin\ccm3.exeon Windows) - Run tests (CCM_BUILD_TESTS defaults to ON):
ctest --test-dir build --output-on-failure— current baseline: 226 tests, all green. - Build tests only:
cmake --build build --target ccm_core_tests - Local coverage env setup (one-time, Windows/MSYS2):
python -m venv .venv_cov
& "P:/msys2/msys64/usr/bin/pacman.exe" -S --noconfirm mingw-w64-ucrt-x86_64-python-lxml mingw-w64-ucrt-x86_64-python-gcovr - Coverage check (core-focused):
cmake -S . -B build-cov -G "MinGW Makefiles" -DCMAKE_BUILD_TYPE=Debug -DCCM_BUILD_TESTS=ON -DCMAKE_C_FLAGS=--coverage -DCMAKE_CXX_FLAGS=--coverage -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
cmake --build build-cov --target ccm_core_tests --parallel
ctest --test-dir build-cov --output-on-failure
& "P:/msys2/msys64/ucrt64/bin/gcovr.exe" -r . --object-directory build-cov --filter "core/" --exclude "build/_deps/" --exclude "build-cov/_deps/" --exclude-directories "build/_deps" --exclude-directories "build-cov/_deps" --print-summary
Windows runtime note:
cpris built as a shared library, sobuild/bin/ends up withlibcpr.dll,libcurl.dll,libzlib.dllnext toccm3.exe. With MinGW-w64 you also needlibgcc_s_seh-1.dllandlibstdc++-6.dllfrom your MSYS2 UCRT64bin/onPATH(or copied alongside the exe) to launch from Explorer.Windows rebuild note: linking
ccm3.exefails withPermission deniedif the app is still running/locked. Closeccm3.exebefore rebuilding app targets.Windows cold-start note: first launch right after a fresh build is often slower than subsequent launches due to cold file cache and Windows security scanning (Defender/SmartScreen) on the new exe/dll set. Warm launches are the meaningful baseline for app-side perf changes.
UI performance guardrails
- Keep first paint responsive: avoid heavy synchronous work in constructors of top-level windows/dialogs.
- For startup, defer non-critical work with
CallAfter(...)so the frame appears before data loading. - Preserve "select first row on startup" behavior without blocking first paint by scheduling the initial selection with
CallAfter(...)instead of selecting synchronously during row rebuild. - Avoid repeated set-list loads when opening Add/Edit: cache Magic sets in
MainFrameand reuse them inCardEditDialog. - Pass preloaded set data to dialogs by pointer/reference, not by value, to avoid copying large vectors on every open.
MainFramedefault window size is 1210×770 (ui_wx/src/MainFrame.cpp).- Saving from Edit in
BaseCardEditDialog: themed Yes/No confirmation when the card changed versus the snapshot taken at dialog open; Add mode does not prompt. - While constructing/populating dialogs with many controls/choices, wrap with
Freeze()/Thaw()and append choice items in bulk (wxArrayString) to reduce layout/repaint churn. - Keep selected-card preview usable when remote lookup fails: show a per-game card-back fallback image (CCM2 parity), not a blank preview panel.
- Card preview round-trips are slow (HTTPS handshake + image GET, often two hosts). The three amortizations in place — all game-agnostic — must stay. The full update mechanic (key-driven invalidation, positive↔negative same-key replacement, eviction, manual cache clearing) is documented in
docs/caching.md→ "Updating cached entries"; do not add a side-channelclearCache(...)API toCardPreviewService— keep updates flowing through cache keys so the in-memory and disk tiers stay aligned automatically.CardPreviewServicekeeps a bounded in-memory LRU (kCacheCapacity) of preview bytes keyed by(game, name, setId, setNo)plus a by-URL cache for the per-game card-back fallback. Re-selecting a row already viewed in this session is decode-only, no HTTP. Source failures are split byPreviewLookupError::Kind:NotFound(the upstream answered cleanly that the record has no image) is negative-cached so subsequent clicks short-circuit to the card-back placeholder without HTTP, whileTransient(HTTP/network/parse) is never cached so a brief outage can recover on the next selection. Editing a lookup-relevant field changes the cache key and invalidates the negative entry automatically.LocalPreviewByteCache(portIPreviewByteCache) extends the LRU with an on-disk byte cache rooted at<exeDir>/.cache/preview-cache/— next to the executable, in the same scope asconfig.json, NOT inside the user-configurabledataStoragepath so previews don't follow the user's collection when the data-storage path is reconfigured (the umbrella.cache/directory is reserved for any future computed-from-network caches). Both positive previews andNotFoundverdicts survive app restarts. Lookup order is memory → disk → source/HTTP; a disk hit (positive or negative) is promoted into the in-memory tier so the follow-up call stays decode-only. Total.binpayload size is capped (default 64 MiB) and oldest-by-mtime entries are evicted when a new write would exceed the cap; tiny.negmarkers are not counted against the cap. The persistent tier is fire-and-forget: any I/O error is swallowed by the adapter so disk problems can never break the preview path.CprHttpClientowns a single long-livedcpr::Session(and therefore a single libcurl easy handle) with keep-alive enabled, so repeat HTTPS calls to the same host (api.scryfall.com,api.tcgdex.net,assets.tcgdex.net,db.ygoprodeck.com,yugipedia.com,ms.yugipedia.com,digimoncard.io,images.digimoncard.io) reuse the existing TLS connection. Concurrent callers are serialized through a mutex — easy handles are not thread-safe and the preview path is single-flight already. Session defaultAccept: */*keeps JSON info APIs and binary image GETs on one client;CardPreviewService::fetchAndCacherejects empty HTTP bodies so a bogus 200 cannot masquerade as a cached preview.
Windows UI theming guardrails
wxWidgetsnative dark-mode behavior on Windows is inconsistent across controls and OS builds; prefer explicit app theming inui_wx/src/Theme.cppplus targeted native hints only where needed.- Treat UI text from domain/services as UTF-8 and convert explicitly at wx boundaries (
wxString::FromUTF8(...)for display,ToStdString(wxConvUTF8)for write-back); do not rely on implicitstd::stringconversions on Windows. - For dialogs (
wxDialog) and frames (wxFrame), apply title-bar dark mode through top-level-window handling (not frame-only handling), otherwise modal window headers stay light. - The
wxListCtrlnative header can ignore dark hints; if native theming is unreliable, use a custom themed header row and preserve key UX parity (single-click sort, edge-drag resize, divider double-click autosize). - Do not apply
Explorerclass theming towxTextCtrlin dark mode; some Windows builds force black typed text. Keep edit controls palette-driven viaapplyPaletteToTextCtrl/hardenTextCtrlNativeThemeinTheme.cpp(opt out of immersive dark mode + parentWM_CTLCOLOREDITsubclass — that message goes to the EDIT's parent, notMainFrame). - Theme modal dialogs explicitly before
ShowModal()(Settings, Create/Edit, image viewer, etc.) so they don't inherit mismatched defaults from Windows. - For button hover/pressed contrast fixes in dark theme, prefer explicit state handling in
Theme.cpp; native Windows button states can override wx colors and produce unreadable white-on-white combinations. - Keep button theming state dynamic across theme switches (Dark <-> Light). Avoid lambdas that permanently capture old theme colors or behavior; stale handlers can make light-mode buttons look wrong.
- After changing
ui_wxtheming behavior, rebuild the final app target (cmake --build build --target ccm --parallel), not justccm_ui_wx, before validating runtime behavior. - If linker fails with
Permission deniedonbuild/bin/ccm3.exe, the app is still running; close it before rebuilding.
Required follow-ups
- After modifying a domain type's fields or JSON layout you must update the matching round-trip test in
tests/domain_json_tests.cppand re-run tests. - After adding a new
.cpptocore/orui_wx/you must add it to that package'sCMakeLists.txt. There is no glob. - After adding a new dependency you must verify its license is compatible with this repository's MIT license before merging.
- After changing SonarQube coverage generation, keep dependency build outputs excluded at gcov discovery time (for example
gcovr --exclude-directories "build/_deps"); output-only excludes are not enough for third-party.gcdafiles. The Sonar scan usessonar.coverage.exclusionsfor**/ui_wx/**and**/app/**so the coverage percentage matches the hermeticccm_core_testssurface (core/); analyzed sources are unchanged for other Sonar metrics. - For new code, keep duplication to an absolute minimum: prefer extracting shared helpers/components instead of copy/paste so Sonar duplication stays comfortably below the quality gate.
- For new code, add or update unit tests so behavior is covered and overall test coverage remains high. Exercise both outcomes of meaningful conditionals (success vs error, empty vs non-empty, cache hit vs miss,
NotFoundvsTransient, early return vs fall-through), not only the happy path — Sonar condition coverage oncore/is a separate signal from line coverage. - For new code, run the local coverage workflow (
build-cov+gcovrwith--filter "core/") and keep core line coverage at or above 80% before opening or updating a PR. When checking coverage locally, also review branch/condition metrics (for examplegcovr ... --txt-metric branchor Sonar's condition coverage on the samecore/surface); there is no repo-wide condition threshold in CI yet — use Sonar's per-file condition list to prioritize gaps. - After adding a new game module you must: (1) extend
Gameenum + string mappings incore/include/ccm/domain/Enums.hpp, (2) register the module inapp/main.cpp, (3) add a directory mapping inapp/main.cpp::dirNameForGame, (4) implement anIGameViewderived class (or<Name>GameView) and add it toAppContext::gameViewsin the composition root. - After changing the per-game seams (
IGameModule,IGameView, theBaseCard*Paneltemplate hooks) you must updatedocs/adding-a-new-game.mdso the canonical "add a new game" walkthrough stays in sync with the code. - After changing
formatTextForFsorparseIndexFromFilenameyou must updatetests/fs_names_tests.cpp— these functions exist to stay byte-compatible with the original Rustutil/fs.rs.
Agent collaboration (Cursor / AI)
- Never
git commitorgit pushunless the user explicitly asked you to commit and/or push (e.g. “commit this”, “push to origin”). Preparing diffs and suggesting commands is fine; performing those Git writes without explicit instruction is not. - Never check out another branch to change it unless the user explicitly asked you to work on that branch. Temporarily checking out another branch read-only (inspect history, compare files, run
git show) is fine without asking; switch back to the working branch before making edits unless instructed otherwise.
Anti-patterns
- Don't include
wx/...headers fromcore/(breaks layering and tests will refuse to build). - Don't add tests that hit real network or real disk; use the fake
ccm::testing::InMemoryFileSystemand the existing http/source fakes. - Don't enable
-Wconversion/-Wsign-conversion; they fight wxWidgets'sintIDs. They were intentionally removed fromcmake/CompilerWarnings.cmake. - Don't use
masterfor FetchContent tags. Bump deliberately. - Don't bump
cprpast1.10.5without re-doing the curl install/export plumbing: cpr 1.11.x addsinstall(EXPORT cprTargets)rules that referencelibcurl_shared, which isn't in any export set when curl is built as a sub-project, breaking configure. The 1.10.5 +HAVE_IOCTLSOCKET_FIONBIO=ONworkaround incmake/Dependencies.cmakeis the verified MinGW-w64 path — do not remove it without an end-to-end Windows build first. - Don't create multiple top-level triggers for the same CI intent (feature or master). Keep one triggered orchestrator workflow (
feature-ci.yml,master-ci.yml) and useworkflow_callreusable workflows for OS-specific splits so GitHub Actions stays a single run per intent.