{"owner":"vnotex","repo":"vnote","hasSkills":true,"hasMcp":false,"mcpConfig":null,"found":["AGENTS.md","CLAUDE.md"],"skills":{"AGENTS.md":"# VNote Agent Development Guide\n\nRoot routing document: prerequisites, build, repo-wide rules, and an index of the module docs.\nModule-specific detail lives in the child `AGENTS.md` that owns the code — see\n[Module Documentation Index](#module-documentation-index).\n\n## Prerequisites\n\n- **Git** (with Git Bash on Windows)\n- **CMake** 3.20+\n- **Qt** 5.x or 6.x (with QtWebEngine)\n- **C++14** compatible compiler (MSVC, GCC, Clang)\n- **clang-format** (optional, for automatic code formatting)\n\n## Setup\n\nAfter cloning the repository, run the init script:\n\n| Platform | Command |\n|----------|---------|\n| Linux/macOS | `bash scripts/init.sh` |\n| Windows | `scripts\\init.cmd` |\n\nThe init script:\n1. Initializes and updates git submodules recursively\n2. Installs pre-commit hook for automatic clang-format on staged C++ files\n3. Sets up vtextedit submodule pre-commit hook\n\n## Build Commands\n\n### Release Build\n```bash\nmkdir build && cd build\ncmake ..\ncmake --build . --config Release\n```\n\n### Debug Build\n```bash\nmkdir build && cd build\ncmake .. -DCMAKE_BUILD_TYPE=Debug\ncmake --build . --config Debug\n```\n\n### Windows (PowerShell)\n```powershell\nNew-Item -ItemType Directory -Force -Path build\nSet-Location build\ncmake .. -GNinja\ncmake --build . --config Release\n```\n\n### Clean Build\n```bash\nrm -rf build && mkdir build && cd build && cmake .. && cmake --build .\n```\nNever repair a stale build dir in place.\n\n### Packaging-Only Build (Skip Tests)\nFor CI artifact-only builds that must not compile the test infrastructure (e.g. Qt6-only test code when building against Qt5):\n```bash\ncmake .. -DVNOTE_BUILD_TESTS=OFF\ncmake --build .\n```\n\nTesting (two suites, two build dirs): [tests/AGENTS.md](tests/AGENTS.md).\n\n---\n\n## Submodule Push Discipline (CRITICAL — read before every push)\n\nVNote pins git submodules (`libs/vxcore`, `libs/vtextedit`, `libs/QHotkey`, `libs/qwindowkit`)\nto specific commits. **CI clones submodules from their own remotes**, so a commit that exists\nonly locally fails every CI job at \"Init Submodules\" (`upload-pack: not our ref <sha>`) before\nany build runs.\n\n### Rule: ALWAYS push the submodule FIRST, then the parent repo.\n\n```bash\ncd libs/vxcore\ngit push origin HEAD:main      # or the appropriate branch\ncd ../..\ngit push                       # only now may the gitlink be pushed\n```\nTo push both at once with verification, use `git push --recurse-submodules=on-demand`, or enforce\nit once per clone:\n\n```bash\ngit config push.recurseSubmodules check   # aborts the parent push if a submodule commit is unpushed\n```\n\n### Before pushing, verify no submodule commit is stranded\n\n```bash\n# For each submodule, confirm local HEAD is not ahead of its remote:\ngit submodule foreach 'git status -sb'\n# A line like \"## main...origin/main [ahead 1]\" means an UNPUSHED submodule commit — push it before pushing vnote.\n\n# Confirm the pinned SHA exists on the submodule remote:\ncd libs/vxcore && git branch -r --contains $(git rev-parse HEAD) && cd ../..\n# Empty output = the commit is NOT on any remote branch yet. DO NOT push vnote until it is.\n```\n\nIf CI is already failing with \"not our ref\", the fix is to push the missing submodule commit (do\n**not** roll back the parent pointer if newer parent commits depend on the new submodule API).\n\n### cmark is vendored twice — bump both pins together\n\nThe same cmark fork (`https://github.com/vnotex/cmark.git`) is pinned by two different submodules:\n\n| Parent submodule | Nested cmark path |\n|---|---|\n| `libs/vtextedit` | `libs/cmark` |\n| `libs/vxcore` | `third_party/cmark` |\n\nOnly **one** is ever compiled in: `libs/CMakeLists.txt` adds `vtextedit` first, which defines the\n`cmark` target unconditionally, so vxcore's `if(NOT TARGET cmark)` guard skips its own copy.\nDiverged pins silently build vxcore against an untested cmark, with no error or warning.\n**Bump both submodules to the same cmark commit in a single change.** [`tests/utils/test_cmark_pin_drift.cpp`](tests/utils/test_cmark_pin_drift.cpp) fails the\nbuild when the two recorded pins differ.\n\n---\n\n## Architecture Overview\n\nVNote uses a **clean architecture** with **Model-View-Controller (MVC)** pattern and dependency\ninjection. Models hold data, Views display it, Controllers handle logic, Services own domain\noperations, and every layer receives a `ServiceLocator&` — there are no singletons.\n\n| Layer | Location | Responsibility | Example |\n|-------|----------|----------------|---------|\n| **Model** | `src/models/` | Data representation, Qt Model/View integration | `NotebookNodeModel` exposes node hierarchy via `QAbstractItemModel` |\n| **View** | `src/views/` | Display data, capture user input, emit signals | `NotebookNodeView` renders tree, emits `nodeActivated` signal |\n| **Controller** | `src/controllers/` | Handle actions, orchestrate Model/View, business logic | `NotebookNodeController` handles new/delete/rename operations |\n| **Service** | `src/core/services/` | Domain operations, data access via vxcore | `NotebookCoreService` wraps vxcore C API for notebook CRUD |\n\n### MVC Rules (MUST FOLLOW)\n\n| Rule | Rationale |\n|------|-----------|\n| **Models MUST NOT** contain UI logic | Models are reusable across different views |\n| **Views MUST NOT** modify data directly | Views only display and emit signals |\n| **Controllers MUST NOT** inherit from QWidget | Controllers are testable without GUI |\n| **All layers receive `ServiceLocator&`** | Enables dependency injection and testing |\n| **Use signals/slots between layers** | Loose coupling between M, V, C |\n\nFull diagram, directory tree, design-decision rationale (including the ViewArea2 framework), and\nsource-wide Qt patterns: see [src/AGENTS.md](src/AGENTS.md).\n\n---\n\n## Code Style Guidelines\n\n### Standards\n- **C++14** standard\n- **Qt 5/6** framework\n- CMake with `CMAKE_AUTOMOC`, `CMAKE_AUTOUIC`, `CMAKE_AUTORCC` enabled\n\n### Formatting\n- 2-space indentation\n- 100 character line limit\n- Pointer alignment right: `int *ptr`, not `int* ptr`\n- Use provided `.clang-format` (auto-applied via pre-commit hook)\n\n### No Hardcoded Colors (enforced)\n\n**Never write a literal color into a `setStyleSheet()` call.** VNote ships 10\nthemes, 6 of them dark; a hardcoded `#RRGGBB`, `rgb()/rgba()` literal, or CSS\ncolor name is correct only in whichever theme its author was running, and it\ncannot follow a runtime theme switch.\n\n`tests/utils/test_hardcoded_color_drift.cpp` is a grep gate over `src/` that\nfails the build on any **stylesheet string** literal containing both a CSS color\nproperty and a literal color value (colors used as *data*, and `QColor` painted\nin a `paintEvent`, are out of scope).\n\nUse `InlineBanner`, the `SeverityText` / `MutedText` dynamic properties, a rule\nin each theme's `interface.qss`, or `ThemeService::paletteColor()`. Do **not**\nuse `setEnabled(false)` to mute text. See\n[src/widgets/AGENTS.md § No Hardcoded Colors in C++](src/widgets/AGENTS.md#no-hardcoded-colors-in-c)\nfor the decision table and the escape hatch.\n\n### Naming Conventions\n\n| Element | Convention | Example |\n|---------|------------|---------|\n| Classes | CamelCase | `ConfigMgr`, `MainWindow` |\n| Methods | camelCase | `getInst()`, `initLoad()` |\n| Parameters | `p_` prefix | `p_parent`, `p_config` |\n| Members | `m_` prefix | `m_themeMgr`, `m_config` |\n| Constants | `c_` prefix | `c_orgName`, `c_appName` |\n| Getters | `get` prefix | `getThemeMgr()`, `getName()` |\n\n### Include Order\n```cpp\n#include \"ownheader.h\"      // Own header first\n\n#include <QDateTime>        // Qt includes\n#include <QObject>\n\n#include \"localheader.h\"    // Local includes\n#include <core/configmgr.h>\n#include <utils/utils.h>\n\nusing namespace vnotex;     // Namespace declaration in .cpp\n```\n\n### Header Guards\n```cpp\n#ifndef CLASSNAME_H\n#define CLASSNAME_H\n// ...\n#endif // CLASSNAME_H\n```\n\n### Namespaces\n\nVNote uses a single `vnotex` namespace. Services that wrap the vxcore C library use the `CoreService` suffix to distinguish them from higher-level wrapper services:\n\n| Class Pattern | Purpose | Examples |\n|---------------|---------|----------|\n| `XXXCoreService` | Low-level services that wrap the vxcore C library (hold `VxCoreContextHandle`) | `ConfigCoreService`, `NotebookCoreService`, `BufferCoreService`, `SearchCoreService`, `FileTypeCoreService` |\n| Other classes | Everything else: UI, controllers, models, hook-aware wrapper services | `BufferService` (hook wrapper), `HookManager`, `TemplateService`, `ConfigMgr2`, controllers, widgets |\n\n**Rules:**\n- `using namespace vnotex;` in `.cpp` files only, never in headers\n- Forward declarations preferred in headers\n\n### Signal/Slot Connections\n```cpp\n// Preferred: new Qt5 syntax\nconnect(m_taskMgr, &TaskMgr::taskOutputRequested,\n        this, &VNoteX::showOutputRequested);\n\n// With overloaded methods\nconnect(this, &VNoteX::openNodeRequested, m_bufferMgr,\n        QOverload<Node *, const QSharedPointer<FileOpenParameters> &>::of(&BufferMgr::open));\n```\n\nMemory management, queued-connection metatype naming (a Qt 5 correctness rule), and the rest of\nthe source-wide patterns: [src/AGENTS.md § Source-Wide Qt Patterns](src/AGENTS.md#source-wide-qt-patterns).\nNoncopyable, `VNOTEX_DEPRECATED` and exception handling: [src/core/AGENTS.md](src/core/AGENTS.md#core-c-facilities).\n\n---\n\n## Sync State Model\n\nNotebook sync has 8 reachable states (S0-S7), defined by the tuple of on-disk JSON sync fields,\nPAT presence in the OS keychain, and runtime registration in vxcore's `states_` map. **S5 is the\nonly \"ready\" state**; S1-S4 and S6 are partial/inconsistent, S0 is cleanly disabled, S7 is\nin-flight. Every controller, widget, and service that touches sync must reason in these terms.\n\nFull predicate table, recovery paths, reconcile semantics, disable cleanup, the S6 startup sweep,\nand the Qt-side scheduling shape:\n[src/core/services/AGENTS.md § Sync State Model](src/core/services/AGENTS.md#sync-state-model).\nvxcore-side threading contract:\n[libs/vxcore/src/sync/AGENTS.md](libs/vxcore/src/sync/AGENTS.md).\n---\n\n## Save Path Threading Contract\n\nBuffer saves run on a worker via `BufferSaveQueue`; save and git-stage/commit work on the SAME\nnotebook are serialized by the per-notebook `NotebookIoGate` async mutex.\n\n> **Forbidden Patterns (post-T7):**\n> - Calling `vxcore_buffer_save` directly from the UI thread. Use [`BufferSaveQueue::enqueue`](src/core/services/buffersavequeue.h) instead.\n> - Touching a notebook's working tree (save, stage, commit, checkout) without holding `NotebookIoGate::ScopedLock(notebookId)`.\n\nFull rationale and the two-phase sync gate:\n[src/core/services/AGENTS.md § Save Path Threading Contract](src/core/services/AGENTS.md#save-path-threading-contract).\n\n---\n\n## Search Threading Contract\n\nContent search in vxcore owns NO thread pool: it enqueues one work item per file-chunk onto the\n`\"vxcore.search\"` work queue, and the CALLER owns the drain policy (VNote's `SearchService` runs\nthe drain pool; the initiating thread help-drains, which is the single-threaded correctness\nfloor).\n\nFull contract: [src/core/services/AGENTS.md § Search Threading Contract](src/core/services/AGENTS.md#search-threading-contract).\n\n---\n\n## Update Check\n\nVNote checks a forge for a newer release and, when one exists, tells the user and offers the\n**release page**. That is the whole feature.\n\n> **VNote never modifies its own install directory, and never downloads anything.** There is\n> no lease file, no staging tree, no journal, no swap, no restart-to-apply, no downloader,\n> and nothing is ever extracted or executed. The only thing the check writes is the\n> `lastUpdateCheckTime` / `skippedUpdateVersion` config values. This invariant is what makes\n> a read-only install location (`/usr/bin`, Program Files, a read-only DMG) launchable\n> (issue #2728) — do not reintroduce install-tree mutation, or a downloader, without\n> replacing this section.\n\nRepo-wide forbidden patterns (they constrain `.github/`, packaging, controllers and widgets\nalike, none of which load the service doc):\n\n- **Never** download, extract, execute or install a release artifact.\n- **Never** write outside the configuration directory as part of an update check.\n- **Never** read `assets[]`; the release page is the only affordance.\n- **Never** give `UpdateService` a `ConfigMgr2` dependency — add the policy to the controller.\n\nRelease CI still publishes manifests, minisign signatures and delta ZIPs (see\n`docs/update-signing.md`); they are the interface for a future *external* updater, not this\nclient. Endpoints, the GitHub/Gitee source table, redirect and allowlist rules, and threading:\n[src/core/services/AGENTS.md § Update Check](src/core/services/AGENTS.md#update-check).\n\n---\n\n## Logging\n\nUse Qt logging macros:\n```cpp\nqDebug() << \"Debug message\";\nqInfo() << \"Info message\";\nqWarning() << \"Warning message\";\nqCritical() << \"Critical error\";\n```\n\n## Code Formatting\n\nThe pre-commit hook automatically formats staged C++ files using clang-format.\n\n**Manual formatting:**\n```bash\nclang-format -i src/core/myfile.cpp\n```\n\n**Excluded from formatting:** `libs/` directory (third-party code)\n\n---\n\n## Shared JSON Keys (SSOT)\n\nCross-boundary JSON keys (vxcore↔Qt) live in `<vxcore/notebook_json_keys.h>`.\nSee [`libs/vxcore/AGENTS.md` § JSON Conventions](libs/vxcore/AGENTS.md#json-conventions)\nfor the SSOT contract and the `test_json_key_drift` regression gate.\n\n---\n\n## Module Documentation Index\n\nDetailed knowledge for each module lives in its own AGENTS.md.\n\n**Where to write new documentation:** default to the child `AGENTS.md` that owns the code (create\none for the directory if it does not exist yet). The root doc is injected into *every* agent turn,\nso anything added here costs context on turns that will never need it. Add to root only when the\nknowledge is genuinely repo-wide — i.e. it constrains callers who will never load the owning\nmodule's doc (as the MVC rules and the update-install invariant do), or it is a build/setup/style\nrule that applies everywhere. Even then, keep root to a short normative summary plus a link, and\nput the full detail in the module doc.\n\n| Module | File | Read this when |\n|--------|------|----------------|\n| Source overview | [src/AGENTS.md](src/AGENTS.md) | You need the architecture diagram, directory tree, design-decision rationale, or a source-wide Qt pattern (memory, queued metatypes) |\n| Core & Services | [src/core/AGENTS.md](src/core/AGENTS.md) | ServiceLocator, DI, Buffer2, hooks, config, themes, adding a service |\n| Services (deep) | [src/core/services/AGENTS.md](src/core/services/AGENTS.md) | Sync state model, save/search threading, update check, notifications |\n| Controllers | [src/controllers/AGENTS.md](src/controllers/AGENTS.md) | Adding or changing a controller; MVC rules for controllers |\n| Models | [src/models/AGENTS.md](src/models/AGENTS.md) | Qt Model/View data representations |\n| Views | [src/views/AGENTS.md](src/views/AGENTS.md) | View conventions, delegate patterns |\n| Widgets | [src/widgets/AGENTS.md](src/widgets/AGENTS.md) | Widget conventions, ViewArea2 framework, styling, construction pattern |\n| GUI Services | [src/gui/AGENTS.md](src/gui/AGENTS.md) | Theme, ViewWindowFactory, GUI utilities |\n| Utilities | [src/utils/AGENTS.md](src/utils/AGENTS.md) | PathUtils, HtmlUtils, FileUtils2 reference |\n| Testing | [tests/AGENTS.md](tests/AGENTS.md) | Writing/running tests in either suite, test mode, fixtures, coverage |\n| CI & Packaging | [.github/AGENTS.md](.github/AGENTS.md) | Workflows, `src/Packaging.cmake`, Windows 7 / Qt 5.15 variant, bundled OpenSSL |\n| vxcore (submodule) | [libs/vxcore/AGENTS.md](libs/vxcore/AGENTS.md) | C library: notebook/config/search backend |\n| vxcore Sync | [libs/vxcore/src/sync/AGENTS.md](libs/vxcore/src/sync/AGENTS.md) | Pluggable sync backend interface (ISyncBackend, SyncManager) |\n| vtextedit (submodule) | [libs/vtextedit/AGENTS.md](libs/vtextedit/AGENTS.md) | Qt editor widget library |\n","CLAUDE.md":"# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## Project Overview\n\nVNote is a Qt-based, cross-platform (Windows/Linux/macOS) note-taking application focused on Markdown. It uses C++14, Qt 6, and CMake 3.20+. The project is undergoing a major architectural migration from singletons to dependency injection via `ServiceLocator`.\n\n## Build Commands\n\n```bash\n# Setup (first time after clone)\nbash scripts/init.sh          # Linux/macOS\nscripts\\init.cmd              # Windows\n\n# Configure + build (Release)\nmkdir build && cd build\ncmake .. -GNinja              # Windows with MSVC: run from VS Developer Command Prompt\ncmake --build . --config Release\n\n# Debug build\ncmake .. -DCMAKE_BUILD_TYPE=Debug\ncmake --build . --config Debug\n\n# Clean rebuild\nrm -rf build && mkdir build && cd build && cmake .. && cmake --build .\n```\n\n## Testing\n\nTests use Qt Test framework. **Always call `vxcore_set_test_mode(1)` before `vxcore_context_create()` in tests** to prevent corrupting real user data.\n\n```bash\n# Build all tests\ncmake --build build --config Release\n\n# Run all tests via CTest\nctest --test-dir build --output-on-failure\n\n# Run a single test (pattern match)\nctest --test-dir build -R test_error\n\n# Run test executable directly (Windows: Qt DLLs must be in PATH)\n./build/tests/core/test_error.exe\n```\n\n### Adding a New Test\n\nUse the `add_qt_test()` helper in `tests/<module>/CMakeLists.txt`:\n\n```cmake\nadd_qt_test(test_myclass\n  SOURCES test_myclass.cpp ${CMAKE_SOURCE_DIR}/src/module/myclass.cpp\n  LINKS core_services vxcore    # Optional extra link libraries\n  GUILESS                       # Optional: headless (QCoreApplication)\n)\n```\n\nTest files use `QTEST_GUILESS_MAIN(tests::TestClassName)` and must end with `#include \"test_filename.moc\"`.\n\n## Architecture\n\n### Dual Architecture (Legacy + New)\n\nThe codebase has two coexisting architectures:\n\n- **Legacy:** Singleton-based (`VNoteX::getInst()`, `ConfigMgr::getInst()`) — do NOT use for new code\n- **New (files with `2` suffix):** Dependency injection via `ServiceLocator` passed through constructors\n\nNew files coexist with legacy via the `2` suffix convention (e.g., `MainWindow2`, `ConfigMgr2`, `Buffer2`).\n\n### MVC + Service Layer\n\n```\nControllers (src/controllers/)    — business logic, QObject (not QWidget), testable\n    ↕ signals/slots\nModels (src/models/)              — QAbstractItemModel subclasses, no UI logic\nViews (src/views/)                — QTreeView/delegates, display only, emit signals\n    ↕\nServiceLocator                    — DI container (non-owning pointers, NOT a singleton)\n    ↕\nServices (src/core/services/)     — wrap vxcore C API with Qt-friendly interface\n    ↕\nvxcore (libs/vxcore/)             — C library: notebook/config/search backend\n```\n\n**MVC rules:** Models must not contain UI logic. Views must not modify data directly. Controllers must not inherit QWidget. All layers receive `ServiceLocator&` via constructor.\n\n### Key Types\n\n| Type | Role |\n|------|------|\n| `ServiceLocator` | DI container; stores non-owning `void*` pointers keyed by `type_index` |\n| `NodeIdentifier` | Lightweight value type: `notebookId` (GUID) + `relativePath` |\n| `Buffer2` | Lightweight copyable handle (like `QModelIndex`), returned by `BufferService::openBuffer()` |\n| `XXXCoreService` | Low-level services wrapping vxcore C API (hold `VxCoreContextHandle`) |\n| `BufferService` | Hook-aware wrapper over `BufferCoreService`; fires `vnote.file.*` hooks |\n| `HookManager` | WordPress-style hook system: actions (cancellable events) + filters (data transforms) |\n\n### Service Registration (main.cpp)\n\nServices are stack-allocated in `main()` within a scoped block, registered as non-owning pointers in `ServiceLocator`, and destroyed before `vxcore_context_destroy()`. Order matters.\n\n### Hook System\n\nPlugins use `HookManager::addAction()` / `addFilter()` with priority ordering. Hook names are constants in `src/core/hooknames.h` (e.g., `vnote.notebook.before_open`, `vnote.file.before_save`). Actions can cancel operations via `HookContext::cancel()`. When adding hooks to existing code, use the WRAP pattern: fire before-hook → original signal → after-hook.\n\n### Git Submodules\n\nThree submodules in `libs/`:\n- **vtextedit** — Rich text/Markdown editor widget\n- **QHotkey** — Cross-platform global hotkey support\n- **vxcore** — C library backend (built as static lib, tests/CLI disabled)\n\n**CRITICAL — push submodules before pushing vnote.** The parent repo pins each submodule to a commit SHA, but CI clones submodules from their own remotes. If you change submodule code, you MUST push the submodule's branch to its remote **before** pushing the vnote commit that bumps the pointer. Otherwise every CI job fails at \"Init Submodules\" with `fatal: remote error: upload-pack: not our ref <sha>`, which breaks all checks before any build runs.\n\n```bash\n# 1) Push the submodule first\ncd libs/vxcore && git push origin HEAD:main && cd ../..\n# 2) Then push vnote. Use a guard to catch stranded submodule commits:\ngit config push.recurseSubmodules check   # one-time: aborts parent push if a submodule commit is unpushed\ngit push --recurse-submodules=on-demand    # or push both together\n# Verify the pinned SHA exists upstream:\ngit submodule foreach 'git status -sb'     # \"[ahead N]\" means an UNPUSHED submodule commit\n```\n\nIf CI already shows \"not our ref\", fix it by pushing the missing submodule commit (do not roll back the parent pointer when newer parent commits depend on the new submodule API).\n\n## Code Style\n\nEnforced by `.clang-format` via pre-commit hook. Key conventions:\n\n- **C++14**, 2-space indent, 100-char line limit\n- **Naming:** `CamelCase` classes, `camelCase` methods, `p_` params, `m_` members, `c_` constants\n- **Pointer alignment:** right (`int *ptr`)\n- **Include order:** own header → Qt → local/project → `using namespace vnotex;` in `.cpp` only\n- **Header guards:** `#ifndef CLASSNAME_H` / `#define CLASSNAME_H`\n- **Namespace:** single `vnotex` namespace; never `using namespace` in headers\n- `libs/` directory is excluded from formatting (third-party code)\n\n### Manual formatting\n\n```bash\nclang-format -i src/core/myfile.cpp\n```\n\n## Directory Layout (New Architecture)\n\n| Directory | Contents |\n|-----------|----------|\n| `src/core/services/` | Service layer (`core_services` static library, links `vxcore`) |\n| `src/core/` | Core types, legacy code, config, hook system |\n| `src/controllers/` | MVC controllers |\n| `src/models/` | MVC models |\n| `src/views/` | MVC views and delegates |\n| `src/widgets/` | UI widgets (receive `ServiceLocator&`), including `dialogs/` |\n| `src/gui/services/` | GUI-aware services (`ThemeService`, `ViewWindowFactory`) |\n| `src/gui/utils/` | GUI utility helpers |\n| `tests/` | Qt Test suites; `helpers/` has `TempDirFixture` and common includes |\n| `libs/` | Git submodules (vtextedit, QHotkey, vxcore) |\n\n## Migration Patterns\n\nWhen migrating legacy code to new architecture:\n1. Create new file with `2` suffix\n2. Replace `ConfigMgr::getInst()` → `m_services.get<ConfigCoreService>()`\n3. Replace `VNoteX::getInst().getNotebookMgr()` → `m_services.get<NotebookCoreService>()`\n4. Add `ServiceLocator &p_services` constructor parameter, store as `m_services`\n5. Add to appropriate `CMakeLists.txt`\n\n## CI\n\nGitHub Actions workflows in `.github/workflows/`: `ci-win.yml`, `ci-linux.yml`, `ci-macos.yml`. Trigger on push/PR to `master`. Windows builds use Ninja + MSVC 2022 with Qt 6.10.3.\n"},"files":{"AGENTS.md":"# VNote Agent Development Guide\n\nRoot routing document: prerequisites, build, repo-wide rules, and an index of the module docs.\nModule-specific detail lives in the child `AGENTS.md` that owns the code — see\n[Module Documentation Index](#module-documentation-index).\n\n## Prerequisites\n\n- **Git** (with Git Bash on Windows)\n- **CMake** 3.20+\n- **Qt** 5.x or 6.x (with QtWebEngine)\n- **C++14** compatible compiler (MSVC, GCC, Clang)\n- **clang-format** (optional, for automatic code formatting)\n\n## Setup\n\nAfter cloning the repository, run the init script:\n\n| Platform | Command |\n|----------|---------|\n| Linux/macOS | `bash scripts/init.sh` |\n| Windows | `scripts\\init.cmd` |\n\nThe init script:\n1. Initializes and updates git submodules recursively\n2. Installs pre-commit hook for automatic clang-format on staged C++ files\n3. Sets up vtextedit submodule pre-commit hook\n\n## Build Commands\n\n### Release Build\n```bash\nmkdir build && cd build\ncmake ..\ncmake --build . --config Release\n```\n\n### Debug Build\n```bash\nmkdir build && cd build\ncmake .. -DCMAKE_BUILD_TYPE=Debug\ncmake --build . --config Debug\n```\n\n### Windows (PowerShell)\n```powershell\nNew-Item -ItemType Directory -Force -Path build\nSet-Location build\ncmake .. -GNinja\ncmake --build . --config Release\n```\n\n### Clean Build\n```bash\nrm -rf build && mkdir build && cd build && cmake .. && cmake --build .\n```\nNever repair a stale build dir in place.\n\n### Packaging-Only Build (Skip Tests)\nFor CI artifact-only builds that must not compile the test infrastructure (e.g. Qt6-only test code when building against Qt5):\n```bash\ncmake .. -DVNOTE_BUILD_TESTS=OFF\ncmake --build .\n```\n\nTesting (two suites, two build dirs): [tests/AGENTS.md](tests/AGENTS.md).\n\n---\n\n## Submodule Push Discipline (CRITICAL — read before every push)\n\nVNote pins git submodules (`libs/vxcore`, `libs/vtextedit`, `libs/QHotkey`, `libs/qwindowkit`)\nto specific commits. **CI clones submodules from their own remotes**, so a commit that exists\nonly locally fails every CI job at \"Init Submodules\" (`upload-pack: not our ref <sha>`) before\nany build runs.\n\n### Rule: ALWAYS push the submodule FIRST, then the parent repo.\n\n```bash\ncd libs/vxcore\ngit push origin HEAD:main      # or the appropriate branch\ncd ../..\ngit push                       # only now may the gitlink be pushed\n```\nTo push both at once with verification, use `git push --recurse-submodules=on-demand`, or enforce\nit once per clone:\n\n```bash\ngit config push.recurseSubmodules check   # aborts the parent push if a submodule commit is unpushed\n```\n\n### Before pushing, verify no submodule commit is stranded\n\n```bash\n# For each submodule, confirm local HEAD is not ahead of its remote:\ngit submodule foreach 'git status -sb'\n# A line like \"## main...origin/main [ahead 1]\" means an UNPUSHED submodule commit — push it before pushing vnote.\n\n# Confirm the pinned SHA exists on the submodule remote:\ncd libs/vxcore && git branch -r --contains $(git rev-parse HEAD) && cd ../..\n# Empty output = the commit is NOT on any remote branch yet. DO NOT push vnote until it is.\n```\n\nIf CI is already failing with \"not our ref\", the fix is to push the missing submodule commit (do\n**not** roll back the parent pointer if newer parent commits depend on the new submodule API).\n\n### cmark is vendored twice — bump both pins together\n\nThe same cmark fork (`https://github.com/vnotex/cmark.git`) is pinned by two different submodules:\n\n| Parent submodule | Nested cmark path |\n|---|---|\n| `libs/vtextedit` | `libs/cmark` |\n| `libs/vxcore` | `third_party/cmark` |\n\nOnly **one** is ever compiled in: `libs/CMakeLists.txt` adds `vtextedit` first, which defines the\n`cmark` target unconditionally, so vxcore's `if(NOT TARGET cmark)` guard skips its own copy.\nDiverged pins silently build vxcore against an untested cmark, with no error or warning.\n**Bump both submodules to the same cmark commit in a single change.** [`tests/utils/test_cmark_pin_drift.cpp`](tests/utils/test_cmark_pin_drift.cpp) fails the\nbuild when the two recorded pins differ.\n\n---\n\n## Architecture Overview\n\nVNote uses a **clean architecture** with **Model-View-Controller (MVC)** pattern and dependency\ninjection. Models hold data, Views display it, Controllers handle logic, Services own domain\noperations, and every layer receives a `ServiceLocator&` — there are no singletons.\n\n| Layer | Location | Responsibility | Example |\n|-------|----------|----------------|---------|\n| **Model** | `src/models/` | Data representation, Qt Model/View integration | `NotebookNodeModel` exposes node hierarchy via `QAbstractItemModel` |\n| **View** | `src/views/` | Display data, capture user input, emit signals | `NotebookNodeView` renders tree, emits `nodeActivated` signal |\n| **Controller** | `src/controllers/` | Handle actions, orchestrate Model/View, business logic | `NotebookNodeController` handles new/delete/rename operations |\n| **Service** | `src/core/services/` | Domain operations, data access via vxcore | `NotebookCoreService` wraps vxcore C API for notebook CRUD |\n\n### MVC Rules (MUST FOLLOW)\n\n| Rule | Rationale |\n|------|-----------|\n| **Models MUST NOT** contain UI logic | Models are reusable across different views |\n| **Views MUST NOT** modify data directly | Views only display and emit signals |\n| **Controllers MUST NOT** inherit from QWidget | Controllers are testable without GUI |\n| **All layers receive `ServiceLocator&`** | Enables dependency injection and testing |\n| **Use signals/slots between layers** | Loose coupling between M, V, C |\n\nFull diagram, directory tree, design-decision rationale (including the ViewArea2 framework), and\nsource-wide Qt patterns: see [src/AGENTS.md](src/AGENTS.md).\n\n---\n\n## Code Style Guidelines\n\n### Standards\n- **C++14** standard\n- **Qt 5/6** framework\n- CMake with `CMAKE_AUTOMOC`, `CMAKE_AUTOUIC`, `CMAKE_AUTORCC` enabled\n\n### Formatting\n- 2-space indentation\n- 100 character line limit\n- Pointer alignment right: `int *ptr`, not `int* ptr`\n- Use provided `.clang-format` (auto-applied via pre-commit hook)\n\n### No Hardcoded Colors (enforced)\n\n**Never write a literal color into a `setStyleSheet()` call.** VNote ships 10\nthemes, 6 of them dark; a hardcoded `#RRGGBB`, `rgb()/rgba()` literal, or CSS\ncolor name is correct only in whichever theme its author was running, and it\ncannot follow a runtime theme switch.\n\n`tests/utils/test_hardcoded_color_drift.cpp` is a grep gate over `src/` that\nfails the build on any **stylesheet string** literal containing both a CSS color\nproperty and a literal color value (colors used as *data*, and `QColor` painted\nin a `paintEvent`, are out of scope).\n\nUse `InlineBanner`, the `SeverityText` / `MutedText` dynamic properties, a rule\nin each theme's `interface.qss`, or `ThemeService::paletteColor()`. Do **not**\nuse `setEnabled(false)` to mute text. See\n[src/widgets/AGENTS.md § No Hardcoded Colors in C++](src/widgets/AGENTS.md#no-hardcoded-colors-in-c)\nfor the decision table and the escape hatch.\n\n### Naming Conventions\n\n| Element | Convention | Example |\n|---------|------------|---------|\n| Classes | CamelCase | `ConfigMgr`, `MainWindow` |\n| Methods | camelCase | `getInst()`, `initLoad()` |\n| Parameters | `p_` prefix | `p_parent`, `p_config` |\n| Members | `m_` prefix | `m_themeMgr`, `m_config` |\n| Constants | `c_` prefix | `c_orgName`, `c_appName` |\n| Getters | `get` prefix | `getThemeMgr()`, `getName()` |\n\n### Include Order\n```cpp\n#include \"ownheader.h\"      // Own header first\n\n#include <QDateTime>        // Qt includes\n#include <QObject>\n\n#include \"localheader.h\"    // Local includes\n#include <core/configmgr.h>\n#include <utils/utils.h>\n\nusing namespace vnotex;     // Namespace declaration in .cpp\n```\n\n### Header Guards\n```cpp\n#ifndef CLASSNAME_H\n#define CLASSNAME_H\n// ...\n#endif // CLASSNAME_H\n```\n\n### Namespaces\n\nVNote uses a single `vnotex` namespace. Services that wrap the vxcore C library use the `CoreService` suffix to distinguish them from higher-level wrapper services:\n\n| Class Pattern | Purpose | Examples |\n|---------------|---------|----------|\n| `XXXCoreService` | Low-level services that wrap the vxcore C library (hold `VxCoreContextHandle`) | `ConfigCoreService`, `NotebookCoreService`, `BufferCoreService`, `SearchCoreService`, `FileTypeCoreService` |\n| Other classes | Everything else: UI, controllers, models, hook-aware wrapper services | `BufferService` (hook wrapper), `HookManager`, `TemplateService`, `ConfigMgr2`, controllers, widgets |\n\n**Rules:**\n- `using namespace vnotex;` in `.cpp` files only, never in headers\n- Forward declarations preferred in headers\n\n### Signal/Slot Connections\n```cpp\n// Preferred: new Qt5 syntax\nconnect(m_taskMgr, &TaskMgr::taskOutputRequested,\n        this, &VNoteX::showOutputRequested);\n\n// With overloaded methods\nconnect(this, &VNoteX::openNodeRequested, m_bufferMgr,\n        QOverload<Node *, const QSharedPointer<FileOpenParameters> &>::of(&BufferMgr::open));\n```\n\nMemory management, queued-connection metatype naming (a Qt 5 correctness rule), and the rest of\nthe source-wide patterns: [src/AGENTS.md § Source-Wide Qt Patterns](src/AGENTS.md#source-wide-qt-patterns).\nNoncopyable, `VNOTEX_DEPRECATED` and exception handling: [src/core/AGENTS.md](src/core/AGENTS.md#core-c-facilities).\n\n---\n\n## Sync State Model\n\nNotebook sync has 8 reachable states (S0-S7), defined by the tuple of on-disk JSON sync fields,\nPAT presence in the OS keychain, and runtime registration in vxcore's `states_` map. **S5 is the\nonly \"ready\" state**; S1-S4 and S6 are partial/inconsistent, S0 is cleanly disabled, S7 is\nin-flight. Every controller, widget, and service that touches sync must reason in these terms.\n\nFull predicate table, recovery paths, reconcile semantics, disable cleanup, the S6 startup sweep,\nand the Qt-side scheduling shape:\n[src/core/services/AGENTS.md § Sync State Model](src/core/services/AGENTS.md#sync-state-model).\nvxcore-side threading contract:\n[libs/vxcore/src/sync/AGENTS.md](libs/vxcore/src/sync/AGENTS.md).\n---\n\n## Save Path Threading Contract\n\nBuffer saves run on a worker via `BufferSaveQueue`; save and git-stage/commit work on the SAME\nnotebook are serialized by the per-notebook `NotebookIoGate` async mutex.\n\n> **Forbidden Patterns (post-T7):**\n> - Calling `vxcore_buffer_save` directly from the UI thread. Use [`BufferSaveQueue::enqueue`](src/core/services/buffersavequeue.h) instead.\n> - Touching a notebook's working tree (save, stage, commit, checkout) without holding `NotebookIoGate::ScopedLock(notebookId)`.\n\nFull rationale and the two-phase sync gate:\n[src/core/services/AGENTS.md § Save Path Threading Contract](src/core/services/AGENTS.md#save-path-threading-contract).\n\n---\n\n## Search Threading Contract\n\nContent search in vxcore owns NO thread pool: it enqueues one work item per file-chunk onto the\n`\"vxcore.search\"` work queue, and the CALLER owns the drain policy (VNote's `SearchService` runs\nthe drain pool; the initiating thread help-drains, which is the single-threaded correctness\nfloor).\n\nFull contract: [src/core/services/AGENTS.md § Search Threading Contract](src/core/services/AGENTS.md#search-threading-contract).\n\n---\n\n## Update Check\n\nVNote checks a forge for a newer release and, when one exists, tells the user and offers the\n**release page**. That is the whole feature.\n\n> **VNote never modifies its own install directory, and never downloads anything.** There is\n> no lease file, no staging tree, no journal, no swap, no restart-to-apply, no downloader,\n> and nothing is ever extracted or executed. The only thing the check writes is the\n> `lastUpdateCheckTime` / `skippedUpdateVersion` config values. This invariant is what makes\n> a read-only install location (`/usr/bin`, Program Files, a read-only DMG) launchable\n> (issue #2728) — do not reintroduce install-tree mutation, or a downloader, without\n> replacing this section.\n\nRepo-wide forbidden patterns (they constrain `.github/`, packaging, controllers and widgets\nalike, none of which load the service doc):\n\n- **Never** download, extract, execute or install a release artifact.\n- **Never** write outside the configuration directory as part of an update check.\n- **Never** read `assets[]`; the release page is the only affordance.\n- **Never** give `UpdateService` a `ConfigMgr2` dependency — add the policy to the controller.\n\nRelease CI still publishes manifests, minisign signatures and delta ZIPs (see\n`docs/update-signing.md`); they are the interface for a future *external* updater, not this\nclient. Endpoints, the GitHub/Gitee source table, redirect and allowlist rules, and threading:\n[src/core/services/AGENTS.md § Update Check](src/core/services/AGENTS.md#update-check).\n\n---\n\n## Logging\n\nUse Qt logging macros:\n```cpp\nqDebug() << \"Debug message\";\nqInfo() << \"Info message\";\nqWarning() << \"Warning message\";\nqCritical() << \"Critical error\";\n```\n\n## Code Formatting\n\nThe pre-commit hook automatically formats staged C++ files using clang-format.\n\n**Manual formatting:**\n```bash\nclang-format -i src/core/myfile.cpp\n```\n\n**Excluded from formatting:** `libs/` directory (third-party code)\n\n---\n\n## Shared JSON Keys (SSOT)\n\nCross-boundary JSON keys (vxcore↔Qt) live in `<vxcore/notebook_json_keys.h>`.\nSee [`libs/vxcore/AGENTS.md` § JSON Conventions](libs/vxcore/AGENTS.md#json-conventions)\nfor the SSOT contract and the `test_json_key_drift` regression gate.\n\n---\n\n## Module Documentation Index\n\nDetailed knowledge for each module lives in its own AGENTS.md.\n\n**Where to write new documentation:** default to the child `AGENTS.md` that owns the code (create\none for the directory if it does not exist yet). The root doc is injected into *every* agent turn,\nso anything added here costs context on turns that will never need it. Add to root only when the\nknowledge is genuinely repo-wide — i.e. it constrains callers who will never load the owning\nmodule's doc (as the MVC rules and the update-install invariant do), or it is a build/setup/style\nrule that applies everywhere. Even then, keep root to a short normative summary plus a link, and\nput the full detail in the module doc.\n\n| Module | File | Read this when |\n|--------|------|----------------|\n| Source overview | [src/AGENTS.md](src/AGENTS.md) | You need the architecture diagram, directory tree, design-decision rationale, or a source-wide Qt pattern (memory, queued metatypes) |\n| Core & Services | [src/core/AGENTS.md](src/core/AGENTS.md) | ServiceLocator, DI, Buffer2, hooks, config, themes, adding a service |\n| Services (deep) | [src/core/services/AGENTS.md](src/core/services/AGENTS.md) | Sync state model, save/search threading, update check, notifications |\n| Controllers | [src/controllers/AGENTS.md](src/controllers/AGENTS.md) | Adding or changing a controller; MVC rules for controllers |\n| Models | [src/models/AGENTS.md](src/models/AGENTS.md) | Qt Model/View data representations |\n| Views | [src/views/AGENTS.md](src/views/AGENTS.md) | View conventions, delegate patterns |\n| Widgets | [src/widgets/AGENTS.md](src/widgets/AGENTS.md) | Widget conventions, ViewArea2 framework, styling, construction pattern |\n| GUI Services | [src/gui/AGENTS.md](src/gui/AGENTS.md) | Theme, ViewWindowFactory, GUI utilities |\n| Utilities | [src/utils/AGENTS.md](src/utils/AGENTS.md) | PathUtils, HtmlUtils, FileUtils2 reference |\n| Testing | [tests/AGENTS.md](tests/AGENTS.md) | Writing/running tests in either suite, test mode, fixtures, coverage |\n| CI & Packaging | [.github/AGENTS.md](.github/AGENTS.md) | Workflows, `src/Packaging.cmake`, Windows 7 / Qt 5.15 variant, bundled OpenSSL |\n| vxcore (submodule) | [libs/vxcore/AGENTS.md](libs/vxcore/AGENTS.md) | C library: notebook/config/search backend |\n| vxcore Sync | [libs/vxcore/src/sync/AGENTS.md](libs/vxcore/src/sync/AGENTS.md) | Pluggable sync backend interface (ISyncBackend, SyncManager) |\n| vtextedit (submodule) | [libs/vtextedit/AGENTS.md](libs/vtextedit/AGENTS.md) | Qt editor widget library |\n","CLAUDE.md":"# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## Project Overview\n\nVNote is a Qt-based, cross-platform (Windows/Linux/macOS) note-taking application focused on Markdown. It uses C++14, Qt 6, and CMake 3.20+. The project is undergoing a major architectural migration from singletons to dependency injection via `ServiceLocator`.\n\n## Build Commands\n\n```bash\n# Setup (first time after clone)\nbash scripts/init.sh          # Linux/macOS\nscripts\\init.cmd              # Windows\n\n# Configure + build (Release)\nmkdir build && cd build\ncmake .. -GNinja              # Windows with MSVC: run from VS Developer Command Prompt\ncmake --build . --config Release\n\n# Debug build\ncmake .. -DCMAKE_BUILD_TYPE=Debug\ncmake --build . --config Debug\n\n# Clean rebuild\nrm -rf build && mkdir build && cd build && cmake .. && cmake --build .\n```\n\n## Testing\n\nTests use Qt Test framework. **Always call `vxcore_set_test_mode(1)` before `vxcore_context_create()` in tests** to prevent corrupting real user data.\n\n```bash\n# Build all tests\ncmake --build build --config Release\n\n# Run all tests via CTest\nctest --test-dir build --output-on-failure\n\n# Run a single test (pattern match)\nctest --test-dir build -R test_error\n\n# Run test executable directly (Windows: Qt DLLs must be in PATH)\n./build/tests/core/test_error.exe\n```\n\n### Adding a New Test\n\nUse the `add_qt_test()` helper in `tests/<module>/CMakeLists.txt`:\n\n```cmake\nadd_qt_test(test_myclass\n  SOURCES test_myclass.cpp ${CMAKE_SOURCE_DIR}/src/module/myclass.cpp\n  LINKS core_services vxcore    # Optional extra link libraries\n  GUILESS                       # Optional: headless (QCoreApplication)\n)\n```\n\nTest files use `QTEST_GUILESS_MAIN(tests::TestClassName)` and must end with `#include \"test_filename.moc\"`.\n\n## Architecture\n\n### Dual Architecture (Legacy + New)\n\nThe codebase has two coexisting architectures:\n\n- **Legacy:** Singleton-based (`VNoteX::getInst()`, `ConfigMgr::getInst()`) — do NOT use for new code\n- **New (files with `2` suffix):** Dependency injection via `ServiceLocator` passed through constructors\n\nNew files coexist with legacy via the `2` suffix convention (e.g., `MainWindow2`, `ConfigMgr2`, `Buffer2`).\n\n### MVC + Service Layer\n\n```\nControllers (src/controllers/)    — business logic, QObject (not QWidget), testable\n    ↕ signals/slots\nModels (src/models/)              — QAbstractItemModel subclasses, no UI logic\nViews (src/views/)                — QTreeView/delegates, display only, emit signals\n    ↕\nServiceLocator                    — DI container (non-owning pointers, NOT a singleton)\n    ↕\nServices (src/core/services/)     — wrap vxcore C API with Qt-friendly interface\n    ↕\nvxcore (libs/vxcore/)             — C library: notebook/config/search backend\n```\n\n**MVC rules:** Models must not contain UI logic. Views must not modify data directly. Controllers must not inherit QWidget. All layers receive `ServiceLocator&` via constructor.\n\n### Key Types\n\n| Type | Role |\n|------|------|\n| `ServiceLocator` | DI container; stores non-owning `void*` pointers keyed by `type_index` |\n| `NodeIdentifier` | Lightweight value type: `notebookId` (GUID) + `relativePath` |\n| `Buffer2` | Lightweight copyable handle (like `QModelIndex`), returned by `BufferService::openBuffer()` |\n| `XXXCoreService` | Low-level services wrapping vxcore C API (hold `VxCoreContextHandle`) |\n| `BufferService` | Hook-aware wrapper over `BufferCoreService`; fires `vnote.file.*` hooks |\n| `HookManager` | WordPress-style hook system: actions (cancellable events) + filters (data transforms) |\n\n### Service Registration (main.cpp)\n\nServices are stack-allocated in `main()` within a scoped block, registered as non-owning pointers in `ServiceLocator`, and destroyed before `vxcore_context_destroy()`. Order matters.\n\n### Hook System\n\nPlugins use `HookManager::addAction()` / `addFilter()` with priority ordering. Hook names are constants in `src/core/hooknames.h` (e.g., `vnote.notebook.before_open`, `vnote.file.before_save`). Actions can cancel operations via `HookContext::cancel()`. When adding hooks to existing code, use the WRAP pattern: fire before-hook → original signal → after-hook.\n\n### Git Submodules\n\nThree submodules in `libs/`:\n- **vtextedit** — Rich text/Markdown editor widget\n- **QHotkey** — Cross-platform global hotkey support\n- **vxcore** — C library backend (built as static lib, tests/CLI disabled)\n\n**CRITICAL — push submodules before pushing vnote.** The parent repo pins each submodule to a commit SHA, but CI clones submodules from their own remotes. If you change submodule code, you MUST push the submodule's branch to its remote **before** pushing the vnote commit that bumps the pointer. Otherwise every CI job fails at \"Init Submodules\" with `fatal: remote error: upload-pack: not our ref <sha>`, which breaks all checks before any build runs.\n\n```bash\n# 1) Push the submodule first\ncd libs/vxcore && git push origin HEAD:main && cd ../..\n# 2) Then push vnote. Use a guard to catch stranded submodule commits:\ngit config push.recurseSubmodules check   # one-time: aborts parent push if a submodule commit is unpushed\ngit push --recurse-submodules=on-demand    # or push both together\n# Verify the pinned SHA exists upstream:\ngit submodule foreach 'git status -sb'     # \"[ahead N]\" means an UNPUSHED submodule commit\n```\n\nIf CI already shows \"not our ref\", fix it by pushing the missing submodule commit (do not roll back the parent pointer when newer parent commits depend on the new submodule API).\n\n## Code Style\n\nEnforced by `.clang-format` via pre-commit hook. Key conventions:\n\n- **C++14**, 2-space indent, 100-char line limit\n- **Naming:** `CamelCase` classes, `camelCase` methods, `p_` params, `m_` members, `c_` constants\n- **Pointer alignment:** right (`int *ptr`)\n- **Include order:** own header → Qt → local/project → `using namespace vnotex;` in `.cpp` only\n- **Header guards:** `#ifndef CLASSNAME_H` / `#define CLASSNAME_H`\n- **Namespace:** single `vnotex` namespace; never `using namespace` in headers\n- `libs/` directory is excluded from formatting (third-party code)\n\n### Manual formatting\n\n```bash\nclang-format -i src/core/myfile.cpp\n```\n\n## Directory Layout (New Architecture)\n\n| Directory | Contents |\n|-----------|----------|\n| `src/core/services/` | Service layer (`core_services` static library, links `vxcore`) |\n| `src/core/` | Core types, legacy code, config, hook system |\n| `src/controllers/` | MVC controllers |\n| `src/models/` | MVC models |\n| `src/views/` | MVC views and delegates |\n| `src/widgets/` | UI widgets (receive `ServiceLocator&`), including `dialogs/` |\n| `src/gui/services/` | GUI-aware services (`ThemeService`, `ViewWindowFactory`) |\n| `src/gui/utils/` | GUI utility helpers |\n| `tests/` | Qt Test suites; `helpers/` has `TempDirFixture` and common includes |\n| `libs/` | Git submodules (vtextedit, QHotkey, vxcore) |\n\n## Migration Patterns\n\nWhen migrating legacy code to new architecture:\n1. Create new file with `2` suffix\n2. Replace `ConfigMgr::getInst()` → `m_services.get<ConfigCoreService>()`\n3. Replace `VNoteX::getInst().getNotebookMgr()` → `m_services.get<NotebookCoreService>()`\n4. Add `ServiceLocator &p_services` constructor parameter, store as `m_services`\n5. Add to appropriate `CMakeLists.txt`\n\n## CI\n\nGitHub Actions workflows in `.github/workflows/`: `ci-win.yml`, `ci-linux.yml`, `ci-macos.yml`. Trigger on push/PR to `master`. Windows builds use Ninja + MSVC 2022 with Qt 6.10.3.\n"},"items":[{"name":"AGENTS.md","path":"AGENTS.md","title":"AGENTS.md","content":"# VNote Agent Development Guide\n\nRoot routing document: prerequisites, build, repo-wide rules, and an index of the module docs.\nModule-specific detail lives in the child `AGENTS.md` that owns the code — see\n[Module Documentation Index](#module-documentation-index).\n\n## Prerequisites\n\n- **Git** (with Git Bash on Windows)\n- **CMake** 3.20+\n- **Qt** 5.x or 6.x (with QtWebEngine)\n- **C++14** compatible compiler (MSVC, GCC, Clang)\n- **clang-format** (optional, for automatic code formatting)\n\n## Setup\n\nAfter cloning the repository, run the init script:\n\n| Platform | Command |\n|----------|---------|\n| Linux/macOS | `bash scripts/init.sh` |\n| Windows | `scripts\\init.cmd` |\n\nThe init script:\n1. Initializes and updates git submodules recursively\n2. Installs pre-commit hook for automatic clang-format on staged C++ files\n3. Sets up vtextedit submodule pre-commit hook\n\n## Build Commands\n\n### Release Build\n```bash\nmkdir build && cd build\ncmake ..\ncmake --build . --config Release\n```\n\n### Debug Build\n```bash\nmkdir build && cd build\ncmake .. -DCMAKE_BUILD_TYPE=Debug\ncmake --build . --config Debug\n```\n\n### Windows (PowerShell)\n```powershell\nNew-Item -ItemType Directory -Force -Path build\nSet-Location build\ncmake .. -GNinja\ncmake --build . --config Release\n```\n\n### Clean Build\n```bash\nrm -rf build && mkdir build && cd build && cmake .. && cmake --build .\n```\nNever repair a stale build dir in place.\n\n### Packaging-Only Build (Skip Tests)\nFor CI artifact-only builds that must not compile the test infrastructure (e.g. Qt6-only test code when building against Qt5):\n```bash\ncmake .. -DVNOTE_BUILD_TESTS=OFF\ncmake --build .\n```\n\nTesting (two suites, two build dirs): [tests/AGENTS.md](tests/AGENTS.md).\n\n---\n\n## Submodule Push Discipline (CRITICAL — read before every push)\n\nVNote pins git submodules (`libs/vxcore`, `libs/vtextedit`, `libs/QHotkey`, `libs/qwindowkit`)\nto specific commits. **CI clones submodules from their own remotes**, so a commit that exists\nonly locally fails every CI job at \"Init Submodules\" (`upload-pack: not our ref <sha>`) before\nany build runs.\n\n### Rule: ALWAYS push the submodule FIRST, then the parent repo.\n\n```bash\ncd libs/vxcore\ngit push origin HEAD:main      # or the appropriate branch\ncd ../..\ngit push                       # only now may the gitlink be pushed\n```\nTo push both at once with verification, use `git push --recurse-submodules=on-demand`, or enforce\nit once per clone:\n\n```bash\ngit config push.recurseSubmodules check   # aborts the parent push if a submodule commit is unpushed\n```\n\n### Before pushing, verify no submodule commit is stranded\n\n```bash\n# For each submodule, confirm local HEAD is not ahead of its remote:\ngit submodule foreach 'git status -sb'\n# A line like \"## main...origin/main [ahead 1]\" means an UNPUSHED submodule commit — push it before pushing vnote.\n\n# Confirm the pinned SHA exists on the submodule remote:\ncd libs/vxcore && git branch -r --contains $(git rev-parse HEAD) && cd ../..\n# Empty output = the commit is NOT on any remote branch yet. DO NOT push vnote until it is.\n```\n\nIf CI is already failing with \"not our ref\", the fix is to push the missing submodule commit (do\n**not** roll back the parent pointer if newer parent commits depend on the new submodule API).\n\n### cmark is vendored twice — bump both pins together\n\nThe same cmark fork (`https://github.com/vnotex/cmark.git`) is pinned by two different submodules:\n\n| Parent submodule | Nested cmark path |\n|---|---|\n| `libs/vtextedit` | `libs/cmark` |\n| `libs/vxcore` | `third_party/cmark` |\n\nOnly **one** is ever compiled in: `libs/CMakeLists.txt` adds `vtextedit` first, which defines the\n`cmark` target unconditionally, so vxcore's `if(NOT TARGET cmark)` guard skips its own copy.\nDiverged pins silently build vxcore against an untested cmark, with no error or warning.\n**Bump both submodules to the same cmark commit in a single change.** [`tests/utils/test_cmark_pin_drift.cpp`](tests/utils/test_cmark_pin_drift.cpp) fails the\nbuild when the two recorded pins differ.\n\n---\n\n## Architecture Overview\n\nVNote uses a **clean architecture** with **Model-View-Controller (MVC)** pattern and dependency\ninjection. Models hold data, Views display it, Controllers handle logic, Services own domain\noperations, and every layer receives a `ServiceLocator&` — there are no singletons.\n\n| Layer | Location | Responsibility | Example |\n|-------|----------|----------------|---------|\n| **Model** | `src/models/` | Data representation, Qt Model/View integration | `NotebookNodeModel` exposes node hierarchy via `QAbstractItemModel` |\n| **View** | `src/views/` | Display data, capture user input, emit signals | `NotebookNodeView` renders tree, emits `nodeActivated` signal |\n| **Controller** | `src/controllers/` | Handle actions, orchestrate Model/View, business logic | `NotebookNodeController` handles new/delete/rename operations |\n| **Service** | `src/core/services/` | Domain operations, data access via vxcore | `NotebookCoreService` wraps vxcore C API for notebook CRUD |\n\n### MVC Rules (MUST FOLLOW)\n\n| Rule | Rationale |\n|------|-----------|\n| **Models MUST NOT** contain UI logic | Models are reusable across different views |\n| **Views MUST NOT** modify data directly | Views only display and emit signals |\n| **Controllers MUST NOT** inherit from QWidget | Controllers are testable without GUI |\n| **All layers receive `ServiceLocator&`** | Enables dependency injection and testing |\n| **Use signals/slots between layers** | Loose coupling between M, V, C |\n\nFull diagram, directory tree, design-decision rationale (including the ViewArea2 framework), and\nsource-wide Qt patterns: see [src/AGENTS.md](src/AGENTS.md).\n\n---\n\n## Code Style Guidelines\n\n### Standards\n- **C++14** standard\n- **Qt 5/6** framework\n- CMake with `CMAKE_AUTOMOC`, `CMAKE_AUTOUIC`, `CMAKE_AUTORCC` enabled\n\n### Formatting\n- 2-space indentation\n- 100 character line limit\n- Pointer alignment right: `int *ptr`, not `int* ptr`\n- Use provided `.clang-format` (auto-applied via pre-commit hook)\n\n### No Hardcoded Colors (enforced)\n\n**Never write a literal color into a `setStyleSheet()` call.** VNote ships 10\nthemes, 6 of them dark; a hardcoded `#RRGGBB`, `rgb()/rgba()` literal, or CSS\ncolor name is correct only in whichever theme its author was running, and it\ncannot follow a runtime theme switch.\n\n`tests/utils/test_hardcoded_color_drift.cpp` is a grep gate over `src/` that\nfails the build on any **stylesheet string** literal containing both a CSS color\nproperty and a literal color value (colors used as *data*, and `QColor` painted\nin a `paintEvent`, are out of scope).\n\nUse `InlineBanner`, the `SeverityText` / `MutedText` dynamic properties, a rule\nin each theme's `interface.qss`, or `ThemeService::paletteColor()`. Do **not**\nuse `setEnabled(false)` to mute text. See\n[src/widgets/AGENTS.md § No Hardcoded Colors in C++](src/widgets/AGENTS.md#no-hardcoded-colors-in-c)\nfor the decision table and the escape hatch.\n\n### Naming Conventions\n\n| Element | Convention | Example |\n|---------|------------|---------|\n| Classes | CamelCase | `ConfigMgr`, `MainWindow` |\n| Methods | camelCase | `getInst()`, `initLoad()` |\n| Parameters | `p_` prefix | `p_parent`, `p_config` |\n| Members | `m_` prefix | `m_themeMgr`, `m_config` |\n| Constants | `c_` prefix | `c_orgName`, `c_appName` |\n| Getters | `get` prefix | `getThemeMgr()`, `getName()` |\n\n### Include Order\n```cpp\n#include \"ownheader.h\"      // Own header first\n\n#include <QDateTime>        // Qt includes\n#include <QObject>\n\n#include \"localheader.h\"    // Local includes\n#include <core/configmgr.h>\n#include <utils/utils.h>\n\nusing namespace vnotex;     // Namespace declaration in .cpp\n```\n\n### Header Guards\n```cpp\n#ifndef CLASSNAME_H\n#define CLASSNAME_H\n// ...\n#endif // CLASSNAME_H\n```\n\n### Namespaces\n\nVNote uses a single `vnotex` namespace. Services that wrap the vxcore C library use the `CoreService` suffix to distinguish them from higher-level wrapper services:\n\n| Class Pattern | Purpose | Examples |\n|---------------|---------|----------|\n| `XXXCoreService` | Low-level services that wrap the vxcore C library (hold `VxCoreContextHandle`) | `ConfigCoreService`, `NotebookCoreService`, `BufferCoreService`, `SearchCoreService`, `FileTypeCoreService` |\n| Other classes | Everything else: UI, controllers, models, hook-aware wrapper services | `BufferService` (hook wrapper), `HookManager`, `TemplateService`, `ConfigMgr2`, controllers, widgets |\n\n**Rules:**\n- `using namespace vnotex;` in `.cpp` files only, never in headers\n- Forward declarations preferred in headers\n\n### Signal/Slot Connections\n```cpp\n// Preferred: new Qt5 syntax\nconnect(m_taskMgr, &TaskMgr::taskOutputRequested,\n        this, &VNoteX::showOutputRequested);\n\n// With overloaded methods\nconnect(this, &VNoteX::openNodeRequested, m_bufferMgr,\n        QOverload<Node *, const QSharedPointer<FileOpenParameters> &>::of(&BufferMgr::open));\n```\n\nMemory management, queued-connection metatype naming (a Qt 5 correctness rule), and the rest of\nthe source-wide patterns: [src/AGENTS.md § Source-Wide Qt Patterns](src/AGENTS.md#source-wide-qt-patterns).\nNoncopyable, `VNOTEX_DEPRECATED` and exception handling: [src/core/AGENTS.md](src/core/AGENTS.md#core-c-facilities).\n\n---\n\n## Sync State Model\n\nNotebook sync has 8 reachable states (S0-S7), defined by the tuple of on-disk JSON sync fields,\nPAT presence in the OS keychain, and runtime registration in vxcore's `states_` map. **S5 is the\nonly \"ready\" state**; S1-S4 and S6 are partial/inconsistent, S0 is cleanly disabled, S7 is\nin-flight. Every controller, widget, and service that touches sync must reason in these terms.\n\nFull predicate table, recovery paths, reconcile semantics, disable cleanup, the S6 startup sweep,\nand the Qt-side scheduling shape:\n[src/core/services/AGENTS.md § Sync State Model](src/core/services/AGENTS.md#sync-state-model).\nvxcore-side threading contract:\n[libs/vxcore/src/sync/AGENTS.md](libs/vxcore/src/sync/AGENTS.md).\n---\n\n## Save Path Threading Contract\n\nBuffer saves run on a worker via `BufferSaveQueue`; save and git-stage/commit work on the SAME\nnotebook are serialized by the per-notebook `NotebookIoGate` async mutex.\n\n> **Forbidden Patterns (post-T7):**\n> - Calling `vxcore_buffer_save` directly from the UI thread. Use [`BufferSaveQueue::enqueue`](src/core/services/buffersavequeue.h) instead.\n> - Touching a notebook's working tree (save, stage, commit, checkout) without holding `NotebookIoGate::ScopedLock(notebookId)`.\n\nFull rationale and the two-phase sync gate:\n[src/core/services/AGENTS.md § Save Path Threading Contract](src/core/services/AGENTS.md#save-path-threading-contract).\n\n---\n\n## Search Threading Contract\n\nContent search in vxcore owns NO thread pool: it enqueues one work item per file-chunk onto the\n`\"vxcore.search\"` work queue, and the CALLER owns the drain policy (VNote's `SearchService` runs\nthe drain pool; the initiating thread help-drains, which is the single-threaded correctness\nfloor).\n\nFull contract: [src/core/services/AGENTS.md § Search Threading Contract](src/core/services/AGENTS.md#search-threading-contract).\n\n---\n\n## Update Check\n\nVNote checks a forge for a newer release and, when one exists, tells the user and offers the\n**release page**. That is the whole feature.\n\n> **VNote never modifies its own install directory, and never downloads anything.** There is\n> no lease file, no staging tree, no journal, no swap, no restart-to-apply, no downloader,\n> and nothing is ever extracted or executed. The only thing the check writes is the\n> `lastUpdateCheckTime` / `skippedUpdateVersion` config values. This invariant is what makes\n> a read-only install location (`/usr/bin`, Program Files, a read-only DMG) launchable\n> (issue #2728) — do not reintroduce install-tree mutation, or a downloader, without\n> replacing this section.\n\nRepo-wide forbidden patterns (they constrain `.github/`, packaging, controllers and widgets\nalike, none of which load the service doc):\n\n- **Never** download, extract, execute or install a release artifact.\n- **Never** write outside the configuration directory as part of an update check.\n- **Never** read `assets[]`; the release page is the only affordance.\n- **Never** give `UpdateService` a `ConfigMgr2` dependency — add the policy to the controller.\n\nRelease CI still publishes manifests, minisign signatures and delta ZIPs (see\n`docs/update-signing.md`); they are the interface for a future *external* updater, not this\nclient. Endpoints, the GitHub/Gitee source table, redirect and allowlist rules, and threading:\n[src/core/services/AGENTS.md § Update Check](src/core/services/AGENTS.md#update-check).\n\n---\n\n## Logging\n\nUse Qt logging macros:\n```cpp\nqDebug() << \"Debug message\";\nqInfo() << \"Info message\";\nqWarning() << \"Warning message\";\nqCritical() << \"Critical error\";\n```\n\n## Code Formatting\n\nThe pre-commit hook automatically formats staged C++ files using clang-format.\n\n**Manual formatting:**\n```bash\nclang-format -i src/core/myfile.cpp\n```\n\n**Excluded from formatting:** `libs/` directory (third-party code)\n\n---\n\n## Shared JSON Keys (SSOT)\n\nCross-boundary JSON keys (vxcore↔Qt) live in `<vxcore/notebook_json_keys.h>`.\nSee [`libs/vxcore/AGENTS.md` § JSON Conventions](libs/vxcore/AGENTS.md#json-conventions)\nfor the SSOT contract and the `test_json_key_drift` regression gate.\n\n---\n\n## Module Documentation Index\n\nDetailed knowledge for each module lives in its own AGENTS.md.\n\n**Where to write new documentation:** default to the child `AGENTS.md` that owns the code (create\none for the directory if it does not exist yet). The root doc is injected into *every* agent turn,\nso anything added here costs context on turns that will never need it. Add to root only when the\nknowledge is genuinely repo-wide — i.e. it constrains callers who will never load the owning\nmodule's doc (as the MVC rules and the update-install invariant do), or it is a build/setup/style\nrule that applies everywhere. Even then, keep root to a short normative summary plus a link, and\nput the full detail in the module doc.\n\n| Module | File | Read this when |\n|--------|------|----------------|\n| Source overview | [src/AGENTS.md](src/AGENTS.md) | You need the architecture diagram, directory tree, design-decision rationale, or a source-wide Qt pattern (memory, queued metatypes) |\n| Core & Services | [src/core/AGENTS.md](src/core/AGENTS.md) | ServiceLocator, DI, Buffer2, hooks, config, themes, adding a service |\n| Services (deep) | [src/core/services/AGENTS.md](src/core/services/AGENTS.md) | Sync state model, save/search threading, update check, notifications |\n| Controllers | [src/controllers/AGENTS.md](src/controllers/AGENTS.md) | Adding or changing a controller; MVC rules for controllers |\n| Models | [src/models/AGENTS.md](src/models/AGENTS.md) | Qt Model/View data representations |\n| Views | [src/views/AGENTS.md](src/views/AGENTS.md) | View conventions, delegate patterns |\n| Widgets | [src/widgets/AGENTS.md](src/widgets/AGENTS.md) | Widget conventions, ViewArea2 framework, styling, construction pattern |\n| GUI Services | [src/gui/AGENTS.md](src/gui/AGENTS.md) | Theme, ViewWindowFactory, GUI utilities |\n| Utilities | [src/utils/AGENTS.md](src/utils/AGENTS.md) | PathUtils, HtmlUtils, FileUtils2 reference |\n| Testing | [tests/AGENTS.md](tests/AGENTS.md) | Writing/running tests in either suite, test mode, fixtures, coverage |\n| CI & Packaging | [.github/AGENTS.md](.github/AGENTS.md) | Workflows, `src/Packaging.cmake`, Windows 7 / Qt 5.15 variant, bundled OpenSSL |\n| vxcore (submodule) | [libs/vxcore/AGENTS.md](libs/vxcore/AGENTS.md) | C library: notebook/config/search backend |\n| vxcore Sync | [libs/vxcore/src/sync/AGENTS.md](libs/vxcore/src/sync/AGENTS.md) | Pluggable sync backend interface (ISyncBackend, SyncManager) |\n| vtextedit (submodule) | [libs/vtextedit/AGENTS.md](libs/vtextedit/AGENTS.md) | Qt editor widget library |\n","category":"root","tokens":3966},{"name":"CLAUDE.md","path":"CLAUDE.md","title":"CLAUDE.md","content":"# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## Project Overview\n\nVNote is a Qt-based, cross-platform (Windows/Linux/macOS) note-taking application focused on Markdown. It uses C++14, Qt 6, and CMake 3.20+. The project is undergoing a major architectural migration from singletons to dependency injection via `ServiceLocator`.\n\n## Build Commands\n\n```bash\n# Setup (first time after clone)\nbash scripts/init.sh          # Linux/macOS\nscripts\\init.cmd              # Windows\n\n# Configure + build (Release)\nmkdir build && cd build\ncmake .. -GNinja              # Windows with MSVC: run from VS Developer Command Prompt\ncmake --build . --config Release\n\n# Debug build\ncmake .. -DCMAKE_BUILD_TYPE=Debug\ncmake --build . --config Debug\n\n# Clean rebuild\nrm -rf build && mkdir build && cd build && cmake .. && cmake --build .\n```\n\n## Testing\n\nTests use Qt Test framework. **Always call `vxcore_set_test_mode(1)` before `vxcore_context_create()` in tests** to prevent corrupting real user data.\n\n```bash\n# Build all tests\ncmake --build build --config Release\n\n# Run all tests via CTest\nctest --test-dir build --output-on-failure\n\n# Run a single test (pattern match)\nctest --test-dir build -R test_error\n\n# Run test executable directly (Windows: Qt DLLs must be in PATH)\n./build/tests/core/test_error.exe\n```\n\n### Adding a New Test\n\nUse the `add_qt_test()` helper in `tests/<module>/CMakeLists.txt`:\n\n```cmake\nadd_qt_test(test_myclass\n  SOURCES test_myclass.cpp ${CMAKE_SOURCE_DIR}/src/module/myclass.cpp\n  LINKS core_services vxcore    # Optional extra link libraries\n  GUILESS                       # Optional: headless (QCoreApplication)\n)\n```\n\nTest files use `QTEST_GUILESS_MAIN(tests::TestClassName)` and must end with `#include \"test_filename.moc\"`.\n\n## Architecture\n\n### Dual Architecture (Legacy + New)\n\nThe codebase has two coexisting architectures:\n\n- **Legacy:** Singleton-based (`VNoteX::getInst()`, `ConfigMgr::getInst()`) — do NOT use for new code\n- **New (files with `2` suffix):** Dependency injection via `ServiceLocator` passed through constructors\n\nNew files coexist with legacy via the `2` suffix convention (e.g., `MainWindow2`, `ConfigMgr2`, `Buffer2`).\n\n### MVC + Service Layer\n\n```\nControllers (src/controllers/)    — business logic, QObject (not QWidget), testable\n    ↕ signals/slots\nModels (src/models/)              — QAbstractItemModel subclasses, no UI logic\nViews (src/views/)                — QTreeView/delegates, display only, emit signals\n    ↕\nServiceLocator                    — DI container (non-owning pointers, NOT a singleton)\n    ↕\nServices (src/core/services/)     — wrap vxcore C API with Qt-friendly interface\n    ↕\nvxcore (libs/vxcore/)             — C library: notebook/config/search backend\n```\n\n**MVC rules:** Models must not contain UI logic. Views must not modify data directly. Controllers must not inherit QWidget. All layers receive `ServiceLocator&` via constructor.\n\n### Key Types\n\n| Type | Role |\n|------|------|\n| `ServiceLocator` | DI container; stores non-owning `void*` pointers keyed by `type_index` |\n| `NodeIdentifier` | Lightweight value type: `notebookId` (GUID) + `relativePath` |\n| `Buffer2` | Lightweight copyable handle (like `QModelIndex`), returned by `BufferService::openBuffer()` |\n| `XXXCoreService` | Low-level services wrapping vxcore C API (hold `VxCoreContextHandle`) |\n| `BufferService` | Hook-aware wrapper over `BufferCoreService`; fires `vnote.file.*` hooks |\n| `HookManager` | WordPress-style hook system: actions (cancellable events) + filters (data transforms) |\n\n### Service Registration (main.cpp)\n\nServices are stack-allocated in `main()` within a scoped block, registered as non-owning pointers in `ServiceLocator`, and destroyed before `vxcore_context_destroy()`. Order matters.\n\n### Hook System\n\nPlugins use `HookManager::addAction()` / `addFilter()` with priority ordering. Hook names are constants in `src/core/hooknames.h` (e.g., `vnote.notebook.before_open`, `vnote.file.before_save`). Actions can cancel operations via `HookContext::cancel()`. When adding hooks to existing code, use the WRAP pattern: fire before-hook → original signal → after-hook.\n\n### Git Submodules\n\nThree submodules in `libs/`:\n- **vtextedit** — Rich text/Markdown editor widget\n- **QHotkey** — Cross-platform global hotkey support\n- **vxcore** — C library backend (built as static lib, tests/CLI disabled)\n\n**CRITICAL — push submodules before pushing vnote.** The parent repo pins each submodule to a commit SHA, but CI clones submodules from their own remotes. If you change submodule code, you MUST push the submodule's branch to its remote **before** pushing the vnote commit that bumps the pointer. Otherwise every CI job fails at \"Init Submodules\" with `fatal: remote error: upload-pack: not our ref <sha>`, which breaks all checks before any build runs.\n\n```bash\n# 1) Push the submodule first\ncd libs/vxcore && git push origin HEAD:main && cd ../..\n# 2) Then push vnote. Use a guard to catch stranded submodule commits:\ngit config push.recurseSubmodules check   # one-time: aborts parent push if a submodule commit is unpushed\ngit push --recurse-submodules=on-demand    # or push both together\n# Verify the pinned SHA exists upstream:\ngit submodule foreach 'git status -sb'     # \"[ahead N]\" means an UNPUSHED submodule commit\n```\n\nIf CI already shows \"not our ref\", fix it by pushing the missing submodule commit (do not roll back the parent pointer when newer parent commits depend on the new submodule API).\n\n## Code Style\n\nEnforced by `.clang-format` via pre-commit hook. Key conventions:\n\n- **C++14**, 2-space indent, 100-char line limit\n- **Naming:** `CamelCase` classes, `camelCase` methods, `p_` params, `m_` members, `c_` constants\n- **Pointer alignment:** right (`int *ptr`)\n- **Include order:** own header → Qt → local/project → `using namespace vnotex;` in `.cpp` only\n- **Header guards:** `#ifndef CLASSNAME_H` / `#define CLASSNAME_H`\n- **Namespace:** single `vnotex` namespace; never `using namespace` in headers\n- `libs/` directory is excluded from formatting (third-party code)\n\n### Manual formatting\n\n```bash\nclang-format -i src/core/myfile.cpp\n```\n\n## Directory Layout (New Architecture)\n\n| Directory | Contents |\n|-----------|----------|\n| `src/core/services/` | Service layer (`core_services` static library, links `vxcore`) |\n| `src/core/` | Core types, legacy code, config, hook system |\n| `src/controllers/` | MVC controllers |\n| `src/models/` | MVC models |\n| `src/views/` | MVC views and delegates |\n| `src/widgets/` | UI widgets (receive `ServiceLocator&`), including `dialogs/` |\n| `src/gui/services/` | GUI-aware services (`ThemeService`, `ViewWindowFactory`) |\n| `src/gui/utils/` | GUI utility helpers |\n| `tests/` | Qt Test suites; `helpers/` has `TempDirFixture` and common includes |\n| `libs/` | Git submodules (vtextedit, QHotkey, vxcore) |\n\n## Migration Patterns\n\nWhen migrating legacy code to new architecture:\n1. Create new file with `2` suffix\n2. Replace `ConfigMgr::getInst()` → `m_services.get<ConfigCoreService>()`\n3. Replace `VNoteX::getInst().getNotebookMgr()` → `m_services.get<NotebookCoreService>()`\n4. Add `ServiceLocator &p_services` constructor parameter, store as `m_services`\n5. Add to appropriate `CMakeLists.txt`\n\n## CI\n\nGitHub Actions workflows in `.github/workflows/`: `ci-win.yml`, `ci-linux.yml`, `ci-macos.yml`. Trigger on push/PR to `master`. Windows builds use Ninja + MSVC 2022 with Qt 6.10.3.\n","category":"root","tokens":1873}]}