Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
6ecd18e
kanban: mark the offline-queue-overflow framework gap with its issue
Aug 16, 2026
0411d8f
kanban: resolve per-project RBAC's design fork -- in-model, not IAuth…
Aug 16, 2026
2dc6eb2
kanban: rung 4 implementation design spec
Aug 16, 2026
34b5bc1
kanban: incorporate Fable 5 spec review -- correct RBAC identity, fil…
Aug 16, 2026
fa391b6
kanban: backend + testkit implementation plan (20 tasks)
Aug 16, 2026
52c0db3
kanban: rung scaffolding (empty lib/server/tests targets)
Aug 16, 2026
4b8e61b
kanban: fix Task 1 per reviewer feedback (Fix Round 1)
Aug 16, 2026
4c733f6
kanban: strong ids, Role enum, error hierarchy
Aug 16, 2026
c596dee
kanban: database setup + 8-table schema migration
Aug 16, 2026
dc42b11
kanban: entities (Light::Field records for all 8 tables)
Aug 16, 2026
1f745a3
kanban: project_dto.hpp -- CreateProject, role management actions
Aug 16, 2026
f757207
kanban: board_dto.hpp -- GetBoard/MoveTaskPosition/task CRUD actions
Aug 16, 2026
ae76ca4
kanban: mutex-guard KanbanAuthorizer's TokenIssuer slot; add test_kan…
Aug 16, 2026
999ee67
kanban: ProjectAdminModel + AuthModel -- project lifecycle, RBAC role…
Aug 16, 2026
6be39e2
kanban: BoardModel -- OpenBoard/GetBoardState/column+swimlane+task CR…
Aug 16, 2026
3b8adbe
kanban: MoveTaskPosition -- WIP limits, position renumbering, exactly…
Aug 16, 2026
12fe5cf
kanban: GetEventsSince -- board_events table, per-action event stamping
Aug 16, 2026
abe91d6
kanban: gate BoardModel's mutating actions on Role::Member (requireRole)
Aug 16, 2026
1bf1ea0
kanban: GetActivity -- journal-derived activity stream, ledger-hit de…
Aug 16, 2026
a5965e6
kanban: shared-instance lifecycle tests (Local/LocalSingleThread/Sock…
Aug 16, 2026
25e07ff
kanban: App bootstrap (RemoteServer + action log + limits wrapper)
Aug 16, 2026
249f96c
testkit: action_driver.hpp -- SeededScript weighted generator + burst…
Aug 17, 2026
4c86ec7
testkit: offline_rig.hpp -- scripted connectivity drop/revive
Aug 17, 2026
2c2e25d
testkit: client_pool.hpp + convergence.hpp -- N-client convergence as…
Aug 17, 2026
5f457c8
kanban: concurrent-move stress test (N=4, ThreadSanitizer, Local rig …
Aug 17, 2026
a42a8b3
kanban: offline DoD tests -- exactly-once under dropped reply, reconn…
Aug 17, 2026
b0d2ca3
kanban: fix-round-1 -- real busy-timeout mix in the SQLite-contention…
Aug 17, 2026
1b1f1cf
kanban: final-review fix round -- close C1/C2 authz holes, stop doubl…
Aug 17, 2026
17f0fd4
kanban: GUI design spec (desktop client, drag-and-drop board, minimal…
Aug 17, 2026
cb69736
fix CI: roleToString's exhaustive switch missing -Wswitch-default's r…
Aug 17, 2026
7cb96e7
fix CI: drop useless static_cast<int64_t> in OpenBoard's ActionKeyTra…
Aug 17, 2026
5ad2f73
fix: bound SetMemberRole/RemoveMember's principal to auth::kMaxPrinci…
Aug 17, 2026
8128467
deps: bump Lightweight to master tip (bbb972a), fixing LASTRADA-Softw…
Aug 17, 2026
5bfad12
fix CI: close real codecov/patch gaps in action_driver.hpp/convergenc…
Aug 18, 2026
bacaf0c
docs: add kanban rung-4 completion plan
Aug 18, 2026
afd27ef
kanban: add GetMyProjects action for the GUI's project list view
Aug 18, 2026
1647093
kanban: add ProjectAdminPresenter/Bridge for the GUI's login and proj…
Aug 18, 2026
2836459
fix: eliminate ProjectAdminBridge's out-of-band _lastCreateName race
Aug 18, 2026
839659b
kanban: add BoardPresenter/Bridge, proving the concurrency invariant …
Aug 18, 2026
f47b033
kanban: wire EventPoller into BoardBridge, fix myRole doc source
Aug 18, 2026
30375d9
kanban: add the desktop GUI (login, project list, board, members) per…
Aug 18, 2026
285e470
kanban: scope TaskDetailPopup's comment list to the tapped task
Aug 18, 2026
668dac3
kanban: wire SqliteOfflineQueue/NetworkMonitor/SyncWorker/ReconnectCo…
Aug 18, 2026
b8978b9
fix: correct BoardBridge offline-stack doc comments per review
Aug 18, 2026
43d5ce4
kanban: surface dead-lettered offline moves in the GUI per the rung's…
Aug 18, 2026
9ff79c7
kanban: test two clients' offline queues replaying interleaved conver…
Aug 18, 2026
38c64a8
kanban: test that a demoted member's next move is rejected and reads …
Aug 18, 2026
ae9ceba
kanban: extend the SQLite contention test to WAL mode, per the rung's…
Aug 18, 2026
6f86f8c
ci: add a minimal ThreadSanitizer leg that actually runs kanban's con…
Aug 18, 2026
3538ac0
fix docs: correct TESTING.md's self-contradicting CI-tiers claim abou…
Aug 18, 2026
bf23787
kanban: record the cascade-journaling decision (causal parent-id, sup…
Aug 18, 2026
56d13a0
kanban: promote causalParentId-must-not-use-seq guidance into the des…
Aug 18, 2026
371a4f6
journal: add causalParentId + isReplaying() to LogEntry/replay(); kan…
Aug 18, 2026
e09b346
kanban: add the rules table and CreateRule/GetRules/DeleteRule DTOs
Aug 18, 2026
b1a1958
kanban: evaluate automation rules on MoveTaskPosition, journaled with…
Aug 18, 2026
3a7118d
kanban: add the rules management GUI view
Aug 18, 2026
0605b45
kanban: wire RulesView into BoardView navigation, fix stale spec line
Aug 18, 2026
106f869
kanban: add attachment metadata actions (AddAttachment/GetAttachments…
Aug 18, 2026
d51400e
fix: bound AddAttachment's filename/contentType/storageKey to column …
Aug 18, 2026
ba1ae47
kanban: add the attachment HTTP side channel, reusing TokenVerifier, …
Aug 18, 2026
787db10
kanban: authorize attachment GET by project role, not just a valid be…
Aug 18, 2026
f15a99a
kanban: add attachment upload/download to the task detail view
Aug 18, 2026
229881f
kanban: test downloadAttachment's real-404 failed() path
Aug 18, 2026
8f8650d
kanban: mark rung 4 complete -- all DoD bullets met, deferred items i…
Aug 18, 2026
49bddf4
fix: validate X-Attachment-Content-Type to close response-header inje…
Aug 18, 2026
a7f8f00
docs: record pre-existing testkit cross-test isolation followup
Aug 18, 2026
f58e281
fix CI: link Qt6::Network onto ladder_kanban_gui_lib for QNetworkAcce…
Aug 18, 2026
bf40860
fix CI: drain BackendRig's Qt event loop before executor teardown; dr…
Aug 19, 2026
2498cda
docs: record two framework-level TSan findings (fixed #127, open #128)
Aug 19, 2026
64ef2e4
fix CI: ActionKeyTraits<OpenBoard>::key() rejects a disengaged projec…
Aug 19, 2026
c48514c
kanban: rewrite TSan stress test to never construct a QtExecutor (fix…
Aug 20, 2026
0fff33a
kanban: raise stress test's completion-drain timeout for real TSan ov…
Aug 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 120 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,126 @@ jobs:
name: coverage-report
path: build/clang-coverage/html/

# ── Linux: kanban's concurrent-move stress test under ThreadSanitizer ──
# test_kanban_stress.cpp's [tsan]-tagged TEST_CASE drives BoardModel through
# a bare morph::bridge::Bridge/morph::backend::LocalBackend directly, on a
# real morph::exec::ThreadPoolExecutor{4}, with zero Qt frames anywhere in
# its call graph (see the test file's own header comment) -- so the "a GUI
# stack under TSan is mostly noise" rationale that keeps the ladder out of
# linux-sanitizers does not apply to this one test. This job builds only
# what that test needs -- MORPH_BUILD_LADDER=ON, MORPH_LADDER_RUNGS=kanban,
# no Qt GUI modules beyond the WebSockets backend the ladder testkit itself
# requires -- to keep it a minimal, fast, TSan-clean addition rather than
# pulling every rung's Qt Quick/QML code into the sanitizer matrix.
#
# (History: an earlier version of this test drove the same scenario through
# BackendRig{Mode::Local, ...}, whose Mode::Local unconditionally
# constructs a real morph::qt::QtExecutor for client-facing callback
# delivery -- morph#128 found 165 ThreadSanitizer warnings bottoming out in
# genuine Qt-internal frames reached through it, undetectable as real bugs
# or false positives from outside a TSan-instrumented Qt build. Rewriting
# the test to never construct a QtExecutor at all sidesteps the ambiguity
# entirely rather than resolving it.)
kanban-tsan:
name: Kanban / ThreadSanitizer
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4

- name: Cache apt packages
uses: actions/cache@v4
with:
path: /var/cache/apt/archives
key: apt-kanban-tsan-${{ hashFiles('.github/workflows/ci.yml') }}
restore-keys: apt-kanban-tsan-

- name: Install Clang ${{ env.CLANG_VERSION }} from apt.llvm.org
run: |
sudo apt-get update -q
# unixodbc-dev + libsqliteodbc: the ladder (built by this job)
# fetches the Lightweight ORM, whose CMake runs
# `pkg_check_modules(ODBC REQUIRED odbc)`, and whose DbFixture
# opens a real `DRIVER=SQLite3` connection at test time (identical
# rationale to linux-sanitizers' coverage-leg step and ladder-tests'
# own install step). Named explicitly rather than relied on from the
# runner image.
# libyaml-cpp-dev + libzip-dev: Lightweight's own CMakeLists.txt does
# `find_package(yaml-cpp)`/`find_package(libzip)` as system CONFIG
# packages, not through CPM (examples/bank/CMakeLists.txt's comment
# on the identical fetch) — without these, configure fails the
# moment MORPH_BUILD_LADDER=ON pulls Lightweight in.
# libgl1-mesa-dev: every other job that configures MORPH_BUILD_QT=ON
# together with MORPH_BUILD_LADDER=ON installs this (linux-sanitizers'
# coverage leg, ladder-tests, linux-all-features) for Qt's GL platform
# integration; carried here for the same reason even though this leg's
# test run itself stays off-GUI.
sudo apt-get install -y ninja-build catch2 libsqlite3-dev \
unixodbc-dev libsqliteodbc libyaml-cpp-dev libzip-dev libgl1-mesa-dev
wget -qO- https://apt.llvm.org/llvm.sh | sudo bash -s -- ${{ env.CLANG_VERSION }}

# Not the distro's Qt: examples/common/CMakeLists.txt requires 6.5+
# unconditionally and Ubuntu 24.04 still ships 6.4.2 — the same gap
# every other job that builds the ladder on Linux already documents.
- name: Install Qt ${{ env.QT_VERSION }}
uses: jurplel/install-qt-action@v4
with:
version: ${{ env.QT_VERSION }}
modules: qtwebsockets
cache: true

- name: Cache sccache
uses: actions/cache@v4
with:
path: /home/runner/.cache/sccache
key: sccache-kanban-tsan-${{ github.sha }}
restore-keys: sccache-kanban-tsan-

- name: Install sccache
run: |
curl -sSL https://github.com/mozilla/sccache/releases/download/v0.9.1/sccache-v0.9.1-x86_64-unknown-linux-musl.tar.gz \
| tar -xz --strip-components=1 -C /usr/local/bin sccache-v0.9.1-x86_64-unknown-linux-musl/sccache

# MORPH_LADDER_RUNGS=kanban (a single rung, not "all"): examples/CMakeLists.txt's
# rung-selection loop (`if(MORPH_LADDER_RUNGS STREQUAL "all" OR _rung IN_LIST
# MORPH_LADDER_RUNGS)`) matches a single-value list correctly, and examples/common
# (the testkit every rung's tests link) is always added regardless of which rungs
# are selected — so this configures and builds only kanban's ladder targets, not
# the whole ladder.
- name: Configure (clang-tsan, kanban only)
run: |
cmake --preset clang-tsan \
-DMORPH_BUILD_QT=ON \
-DMORPH_BUILD_LADDER=ON \
-DMORPH_LADDER_RUNGS=kanban \
-DCMAKE_C_COMPILER=clang-${{ env.CLANG_VERSION }} \
-DCMAKE_CXX_COMPILER=clang++-${{ env.CLANG_VERSION }} \
-DCMAKE_C_COMPILER_LAUNCHER=sccache \
-DCMAKE_CXX_COMPILER_LAUNCHER=sccache

# QT_QPA_PLATFORM=offscreen here too, not just on Test below: Catch2's
# catch_discover_tests() runs ladder_kanban_tests once at BUILD time to
# enumerate its cases, which can abort on this headless runner (no X
# server) without it — see linux-sanitizers' and ladder-tests' own Build
# steps for the identical note.
- name: Build
env:
QT_QPA_PLATFORM: offscreen
run: cmake --build --preset clang-tsan

# Every ladder ctest case only ever carries the "ladder"/"ladder-<rung>"
# labels morph_add_rung() applies (cmake/morph_add_rung.cmake) — Catch2's
# own tags ([kanban][stress][tsan]) are never translated into ctest
# labels anywhere in this repo's CMake (no catch_discover_tests call
# passes ADD_TAGS_AS_LABELS). A "-L tsan" filter would therefore match
# zero tests and silently run nothing. This test's name is the only
# thing distinguishing it, and "ThreadSanitizer" appears in exactly one
# TEST_CASE name across the whole kanban tree (confirmed by grep), so
# -R selects it precisely.
- name: Test (kanban's TSan-tagged stress test only)
env:
QT_QPA_PLATFORM: offscreen
run: ctest --preset clang-tsan -L ladder-kanban -R ThreadSanitizer --output-on-failure

# ── Linux: Qt WebSocket backend build + tests ─────────────────────────
linux-qt:
name: Linux / Qt6 WebSockets
Expand Down
84 changes: 50 additions & 34 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,48 @@ target_sources(morph
include/morph/render/locale_format.hpp
)

# ── SQLite-backed durable offline queue: library target (optional) ──────────
# Only the morph::offline_sqlite target itself is created here, deliberately
# ahead of "Application ladder"'s add_subdirectory(examples) further below:
# examples/kanban/CMakeLists.txt links morph::offline_sqlite (a namespaced
# target, which CMake requires to already exist at the point it is named --
# see the identical note on morph::qt_forms vs the plain
# morph_forms_moduleplugin just below). The tests/offline_sqlite subdirectory
# that also depends on MORPH_BUILD_OFFLINE_SQLITE stays deferred to its
# original spot, after the "Tests" section, since it links
# Catch2::Catch2WithMain and that target only exists once Catch2 has been
# found/fetched there.
if(MORPH_BUILD_OFFLINE_SQLITE)
find_package(SQLite3 REQUIRED)

# FindSQLite3 reports success but does not always leave `SQLite3::SQLite3`
# resolvable at generate time (observed on the ubuntu-24.04 runner, which
# fails with "the link interface ... contains SQLite3::SQLite3 but the
# target was not found" while printing "Found SQLite3" moments earlier).
# Synthesise the target from the variables the module *does* set, and
# promote it to GLOBAL either way so every subdirectory that consumes
# morph::offline_sqlite can see it.
if(NOT TARGET SQLite3::SQLite3)
add_library(SQLite3::SQLite3 UNKNOWN IMPORTED GLOBAL)
set_target_properties(SQLite3::SQLite3 PROPERTIES
IMPORTED_LOCATION "${SQLite3_LIBRARIES}"
INTERFACE_INCLUDE_DIRECTORIES "${SQLite3_INCLUDE_DIRS}")
else()
set_target_properties(SQLite3::SQLite3 PROPERTIES IMPORTED_GLOBAL TRUE)
endif()

add_library(morph_offline_sqlite INTERFACE)
add_library(morph::offline_sqlite ALIAS morph_offline_sqlite)
target_link_libraries(morph_offline_sqlite INTERFACE morph SQLite3::SQLite3)
target_sources(morph_offline_sqlite
INTERFACE
FILE_SET HEADERS
BASE_DIRS include
FILES
include/morph/offline/sqlite_offline_queue.hpp
)
endif()

# ── Qt/QML forms renderer setup (optional) ───────────────────────────────────
# Ships the reference Qt/QML renderer (MorphForms, src/qt/forms) as a reusable
# component, independent of MORPH_BUILD_EXAMPLES. examples/forms/gui_qml
Expand Down Expand Up @@ -420,40 +462,14 @@ if(MORPH_BUILD_NET)
endif()
endif()

# ── SQLite-backed durable offline queue (optional) ──────────────────────────
if(MORPH_BUILD_OFFLINE_SQLITE)
find_package(SQLite3 REQUIRED)

# FindSQLite3 reports success but does not always leave `SQLite3::SQLite3`
# resolvable at generate time (observed on the ubuntu-24.04 runner, which
# fails with "the link interface ... contains SQLite3::SQLite3 but the
# target was not found" while printing "Found SQLite3" moments earlier).
# Synthesise the target from the variables the module *does* set, and
# promote it to GLOBAL either way so every subdirectory that consumes
# morph::offline_sqlite can see it.
if(NOT TARGET SQLite3::SQLite3)
add_library(SQLite3::SQLite3 UNKNOWN IMPORTED GLOBAL)
set_target_properties(SQLite3::SQLite3 PROPERTIES
IMPORTED_LOCATION "${SQLite3_LIBRARIES}"
INTERFACE_INCLUDE_DIRECTORIES "${SQLite3_INCLUDE_DIRS}")
else()
set_target_properties(SQLite3::SQLite3 PROPERTIES IMPORTED_GLOBAL TRUE)
endif()

add_library(morph_offline_sqlite INTERFACE)
add_library(morph::offline_sqlite ALIAS morph_offline_sqlite)
target_link_libraries(morph_offline_sqlite INTERFACE morph SQLite3::SQLite3)
target_sources(morph_offline_sqlite
INTERFACE
FILE_SET HEADERS
BASE_DIRS include
FILES
include/morph/offline/sqlite_offline_queue.hpp
)

if(MORPH_BUILD_TESTS)
add_subdirectory(tests/offline_sqlite)
endif()
# ── SQLite-backed durable offline queue: tests (optional) ───────────────────
# The morph::offline_sqlite target itself is created much earlier (see
# "SQLite-backed durable offline queue: library target" above, before
# "Application ladder"'s add_subdirectory(examples)) -- only this suite's own
# subdirectory stays deferred to here, since it links Catch2::Catch2WithMain
# and Catch2 is only found/fetched in the "Tests" section just above.
if(MORPH_BUILD_OFFLINE_SQLITE AND MORPH_BUILD_TESTS)
add_subdirectory(tests/offline_sqlite)
endif()

# ── Documentation ───────────────────────────────────────────────────────────
Expand Down
31 changes: 31 additions & 0 deletions cmake/morph_add_rung.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,15 @@ function(morph_add_rung)
if(AF_COVERAGE)
apply_coverage(ladder_${_rung}_lib)
endif()
# AF_SANITIZER (asan/tsan/ubsan): applied the same way apply_coverage()
# is above. Without this, a --preset clang-tsan build of the ladder
# compiles this target (a rung's models -- the code kanban-tsan's
# stress test actually races on) with no sanitizer instrumentation at
# all, silently defeating the whole point of building under that
# preset. See .github/workflows/ci.yml's kanban-tsan job.
if(DEFINED AF_SANITIZER)
apply_sanitizers(ladder_${_rung}_lib ${AF_SANITIZER})
endif()
endif()
endif()

Expand Down Expand Up @@ -178,6 +187,10 @@ function(morph_add_rung)
if(AF_COVERAGE)
apply_coverage(ladder_${_rung}_gui_lib)
endif()
# See ladder_${_rung}_lib's identical AF_SANITIZER block above.
if(DEFINED AF_SANITIZER)
apply_sanitizers(ladder_${_rung}_gui_lib ${AF_SANITIZER})
endif()
endif()

# ── ladder_<rung>_qml: the rung's own QML module ─────────────────────
Expand Down Expand Up @@ -291,6 +304,10 @@ function(morph_add_rung)
if(AF_COVERAGE)
apply_coverage(ladder_${_rung}_gui)
endif()
# See ladder_${_rung}_lib's identical AF_SANITIZER block above.
if(DEFINED AF_SANITIZER)
apply_sanitizers(ladder_${_rung}_gui ${AF_SANITIZER})
endif()
endif()
endif()

Expand Down Expand Up @@ -363,6 +380,10 @@ function(morph_add_rung)
if(AF_COVERAGE)
apply_coverage(ladder_${_rung}_server)
endif()
# See ladder_${_rung}_lib's identical AF_SANITIZER block above.
if(DEFINED AF_SANITIZER)
apply_sanitizers(ladder_${_rung}_server ${AF_SANITIZER})
endif()
endif()
endif()

Expand Down Expand Up @@ -458,6 +479,16 @@ function(morph_add_rung)
if(AF_COVERAGE)
apply_coverage(ladder_${_rung}_tests)
endif()
# AF_SANITIZER (asan/tsan/ubsan): applied the same way apply_coverage()
# is above. This is the target kanban's [tsan]-tagged stress test
# actually links and runs from -- without this, a --preset clang-tsan
# build compiles it with no sanitizer instrumentation at all, and
# .github/workflows/ci.yml's kanban-tsan job would build and pass
# while never actually exercising ThreadSanitizer over the code it
# claims to cover.
if(DEFINED AF_SANITIZER)
apply_sanitizers(ladder_${_rung}_tests ${AF_SANITIZER})
endif()

include(Catch)
get_target_property(_qt_core_dll Qt6::Core IMPORTED_LOCATION)
Expand Down
Loading
Loading