Application ladder: design docs + rung 0 shared infrastructure - #41
Open
Yaraslaut wants to merge 151 commits into
Open
Application ladder: design docs + rung 0 shared infrastructure#41Yaraslaut wants to merge 151 commits into
Yaraslaut wants to merge 151 commits into
Conversation
Eight planned example applications of gradually increasing complexity, each anchored to existing open source projects (MicroBin, linkding, Rallly, Kanboard, Firefly III/Actual Budget, SENAITE/InvenTree/ODK, EspoCRM/Tryton/Frappe, Gogs/Gitea), sequenced so every morph subsystem is stressed by at least two rungs and ending at the forge/CRM class. Each rung README records what to implement, the references to study, the framework limits it is expected to hit, required tests, and design questions to resolve in writing. The plan was hardened by six review rounds (edge cases, adversarial concurrency, GUI testing, delivery realism, forms/units capability mapping, and a verification pass) whose corrections and framework-gap findings are folded into the documents. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HffixfknmXdAjnM5hSGRBb Signed-off-by: Yaraslau Tamashevich <yaraslau.tamashevich@gmail.com>
Binding convention for every ladder rung: presenter-shaped GUIs in a Qt-Core-only library, one test body run across three backend modes (LocalBackend, single-thread WASM-parity, and QtWebSocketBackend against an in-test RemoteServer with N clients), a no-sleep pumping discipline, a convergence assertion for multi-client stress, and the shared examples/common testkit with per-component "first needed by" ordering. Records the verified current state (zero GUI tests, bank's local-only WASM build, the SimulatedRemoteBackend connection-scope caveat), the fault-injection proxy and strand-interleaver harnesses, CMake/CI tiering, and the framework gaps the strategy exposes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HffixfknmXdAjnM5hSGRBb Signed-off-by: Yaraslau Tamashevich <yaraslau.tamashevich@gmail.com>
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Binding rules for how every rung is written: models are the application (all logic and persistence access in plain typed-action models; morph exposes them); GUIs stay minimal and schema-driven, with custom widgets forbidden unless they document a forms-subsystem gap; DTO fields use strong types exclusively (Quantity, Rational, Timestamp, Choice, strong ids, enum class) with std::string as the only permitted plain type; persistence goes through the Lightweight ORM exclusively (entities, DataMapper, LIGHTWEIGHT_SQL_MIGRATION, relations-based ownership - no hand-written database code); models are 100% unit tested across the backend-mode matrix. Includes a per-rung PR checklist and aligns LADDER.md, TESTING.md, and the pastebin rung with the new rules. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HffixfknmXdAjnM5hSGRBb Signed-off-by: Yaraslau Tamashevich <yaraslau.tamashevich@gmail.com>
Fold in the round-7 principal review of the overall idea and design. The committed build scope is rungs 0-4 plus five no-app spikes (forms conformance, Rational fuzz, journal payload evolution, extension bag, forge load script); rungs 5-8 become a design annex whose READMEs are the deliverable and whose construction is a post-rung-4 decision. New examples/FINDINGS.md defines the finding pipeline the review found load-bearing but undefined: finding format, triage dispositions, fix budget, rung exit criteria (feature completeness explicitly is not one), and the CI demotion policy for harvested rungs. Resolves the review's rule tensions: a sanctioned Lightweight escape tier with pre-enumerated escapees (rung 1's recommended burn-atomicity answer was illegal as written); a strong-type palette row for protocol scalars (cursors, event ids, op-ids, tokens); store-error coverage via a db_fault_fixture instead of a silent gate weakening; the dual-mode GUI rig reframed as a conformance harness for morph's client-side stack owned by the testkit; the IndexedDB queue stretch goal declared framework-candidate code; and a rule-of-three promotion rule so recurring app-built answers graduate into morph instead of accreting as a shadow framework. The fault-injection proxy and strand interleaver move to rung 0-1; kanban is named the single polished showcase. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HffixfknmXdAjnM5hSGRBb Signed-off-by: Yaraslau Tamashevich <yaraslau.tamashevich@gmail.com>
MORPH_BUILD_LADDER's add_subdirectory(examples) previously ran before the Tests section's Catch2 find_package/FetchContent fallback, so examples/common/CMakeLists.txt's own find_package(Catch2 3 CONFIG QUIET) could run (and hard FATAL_ERROR) before Catch2 had any chance to be fetched -- reproducible with no system Catch2 package installed. Move the ladder's add_subdirectory(examples) to after the Tests section, mirroring the existing MORPH_BUILD_FORMS_QML/src/qt/forms deferral for the identical reason.
…t's SqlTestFixture) DbFixture shares one real, on-disk SQLite database per test binary and resets it in its constructor by dropping every table and re-applying pending migrations, matching Lightweight's own SqlTestFixture (Lightweight/src/tests/Utils.hpp) and bank_test_support.hpp's ensureDatabase() rather than a fresh temp file per test. Adapted from the plan's illustrative draft after cross-checking real headers: DataMapper's default table name falls back to the reflected struct name, so the probe entity needs an explicit TableName matching the migration's snake_case table; and reflection-cpp requires external linkage for reflected types, so the probe struct lives in a named namespace instead of an anonymous one (Lightweight's own MigrationReflectionTests.cpp hits the same constraint). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HffixfknmXdAjnM5hSGRBb
… contention) Wraps DbFixture plus a second, independent SqlConnection holding a real Lightweight SqlScopedLock, so store-error coverage (examples/IMPLEMENTATION.md rule 5) can exercise genuine cross-session lock contention instead of a hand-rolled mock. Mirrors Lightweight's own MigrationLockTests.cpp idiom. SqlScopedLock::Name() returns std::string_view (not a std::string&, as the plan's illustrative draft had it), so DbFaultFixture::lockName() is typed accordingly.
Adds BackendRig, the three-mode fixture that lets one Catch2 test body run against every deployment shape the ladder ships: Local (shared in-process Bridge over a ThreadPoolExecutor), LocalSingleThread (WASM constraint-parity mode), and Socket (RemoteServer + QtWebSocketServer over a real loopback socket, one Bridge per client). Fixes two issues found in the plan's draft during implementation: - Mode::Local's per-client construction loop was dead/misleading code (a self-move ternary that only ever built one bridge, on iteration 0). Hoisted bridge construction out of the loop entirely — Local mode needs exactly one LocalBackend/Bridge regardless of nClients, since every client index shares it. - Mode::LocalSingleThread would hang under pump.hpp's pumpUntil/awaitQt: those only pump the Qt event loop and never call MainThreadExecutor::runFor(), so work LocalBackend posts onto a raw MainThreadExecutor would never run. Added QtDrivenMainThreadExecutor, a small adapter that schedules a bounded runFor() via a zero-delay QTimer on every post(), so LocalSingleThread mode drains through the existing pumping discipline instead of requiring a caller to manually call runFor(). Also links morph_qt_impl into morph_ladder_testkit: BackendRig's Socket mode is the first user of QtWebSocketServer/QtWebSocketBackend in examples/common, and their compiled implementations live in that separate static library (morph::qt is header-only). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HffixfknmXdAjnM5hSGRBb
RigProbeModel's execute() is a pure function of its action (no member
state), so the "N clients each get an isolated model instance" test built on
it could not actually distinguish genuine per-client isolation from every
client accidentally sharing one server-side instance -- it would pass either
way. Add RigCounterModel/RigAddAction (an int accumulator, mirroring
tests/qt/test_qt_websocket.cpp's WsCounterModel/WsAddAction) and rewrite the
test to drive each of the 3 Socket-mode clients through a different number of
increments, asserting each client's final running total. Only true
per-client isolation produces exactly {30, 2, 20}; any accidental sharing
would contaminate at least one client's total.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HffixfknmXdAjnM5hSGRBb
AppContext owns worker pool -> QtExecutor -> Bridge (destroyed in reverse, via member declaration order) and is parameterized over Local/Remote backend modes, replacing bank's hard-wired LocalBackend. login() forwards the principal to Bridge::setDefaultSession via session::Context. Presenter is a Q_OBJECT base tracking in-flight track()ed completions via an atomic counter, exposing busy()/idle() so tests can wait for quiescence instead of sleeping. morph_ladder_gui now links morph::qt + morph_qt_impl (AppContext needs QtWebSocketBackend/QtExecutor, previously only Qt6::Core was linked).
FaultProxy is an in-process WebSocket relay sitting between a QtWebSocketBackend and the real QtWebSocketServer, forwarding every frame verbatim except where a rule armed for a reply's callId intercepts it: dropReply, delayReply, duplicateReply, killAfter. A test cannot name "call k" from the outside — BridgeHandler::execute() returns a bare Completion and never exposes the callId the backend assigned it. setRequestObserver closes that gap race-free: it fires from the client->server forwarding path, after decoding a request's callId but before the request is forwarded upstream, so a rule armed from the callback is installed strictly before the server can produce a reply for it. Two structural details the relay needs beyond plain forwarding: client frames are buffered until the proxy's own upstream handshake completes (the first frame a client sends is a synchronous register, emitted the moment waitForConnected() returns, and a write to a still-opening socket is lost), and a killed client leg has its signals detached before abort() so a queued disconnected cannot null out a leg the client's reconnect already installed. repliesForwarded() counts server->client frames on the wire, which is what lets the duplicate test tell "the Completion ignored the second copy" apart from "no second copy was ever sent". Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HffixfknmXdAjnM5hSGRBb
The dropReply test deliberately leaves call 2's Completion unsettled at scope exit — that is the assertion. But `secondResolved`/`secondFailed` were declared after `ProxyRig rig`, so they were destroyed *first* (reverse declaration order), and `~ProxyRig` then tore the backend down: cancelPending posts the .onError lambda through QtExecutor, and ~QtWebSocketBackend's own processEvents() dispatches it — writing into stack slots that no longer existed. Confirmed rather than assumed: with the rig moved into an inner scope and the flags left outside it, `secondFailed` is observably true once the inner scope closes, so the teardown write does land. Pre-fix that same write had no live storage to land in. Every by-reference-captured local in the file now lives above its rig — the request observer's counters (the proxy owns that lambda until ~ProxyRig destroys it) and the completion flags in all four tests, not just the one currently reachable. The other three settle their completions before returning, but only if their REQUIREs hold, and a failing REQUIRE unwinds the scope with a completion still pending — a failing test must not also be undefined behaviour. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HffixfknmXdAjnM5hSGRBb
awaitQt captured its value/error locals by reference in the then()/ onError() handlers registered on the completion. Those handlers are held by the completion's backing CompletionState, which can outlive awaitQt's own stack frame: if pumpUntil times out, awaitQt throws and unwinds while the underlying operation is still pending. A callback firing after that unwind wrote through a dangling reference into destroyed stack memory. Move value/error into a heap-allocated State behind a shared_ptr, captured by value in both handlers, so a late callback writes into orphaned-but-valid heap memory instead. Add a regression test that lets awaitQt time out on a still-alive completion, then resolves it afterward and pumps, to exercise the late-callback path. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HffixfknmXdAjnM5hSGRBb
DeterministicExecutor is a header-only morph::exec::IExecutor that queues
every posted task and runs them only when explicitly stepped, so a test can
script an exact interleaving instead of depending on OS thread scheduling.
Sits underneath a StrandExecutor as its base executor to make strand-
ordering bugs (kanban's MoveTaskPosition centerpiece, a later rung)
reproducible rather than probabilistic. Companion harness to Task 7's
fault-injection wire proxy for finding 004.
strand_interleaver.cpp (a one-line placeholder from Task 1) is removed:
the class is fully header-defined, is not a QObject, and the testkit
library already links other real TUs, so an empty .cpp would be dead
weight.
Along the way, found and fixed an off-by-one in the plan's draft
runSchedule test: indices are consumed against the *current* (shrinking)
queue as each entry is erased, not the original snapshot, so {2, 0, 1}
throws where {2, 0, 0} is correct. Added a third test that hand-traces
StrandExecutor's real post()/scheduleNext() behavior to force a genuinely
non-default two-key interleaving via runSchedule, plus direct throw-path
coverage for step() on an empty queue and runSchedule() with an
out-of-range index.
Review flagged runSchedule as missing a @param order tag: unlike post() (which overrides an already-documented IExecutor::post), runSchedule is a novel public method and needs its own complete Doxygen docs per CLAUDE.md's Docs CI note (WARN_AS_ERROR = FAIL_ON_WARNINGS).
…de/morph)
Builds and runs ladder_common_tests (23 cases, ctest labels ladder/ladder-0)
against gcc-debug with MORPH_BUILD_LADDER=ON, mirroring linux-qt's install
steps. Skips its build/test steps entirely unless the diff against the PR
base (or push's before-sha) touches examples/{common,pastebin,bookmarks,
polls,kanban}/, include/morph/, or the ladder design docs.
…cripten)
Task 10 of rung 0 (examples/LADDER.md): the WASM-remote spike proving
morph::qt::QtWebSocketBackend works from a WASM-compiled client, which
per examples/TESTING.md's "WASM reality" section has never been
exercised in this repo before.
- examples/common/wasm_spike/{spike_model.hpp,main_wasm.cpp,
CMakeLists.txt,README.md}: a minimal QCoreApplication + QTimer WASM
client that registers SpikeEchoModel and executes one round-trip
action against a remote server, using asyncRegistrationEnabled=true
and setConnectHandler (the two WASM-mandatory patterns). The
CMakeLists.txt only requires Qt6 Core (morph::qt pulls in
Qt6::WebSockets itself) -- fixes the brief's draft, which
overspecified Qml/Quick copied from bank's WASM GUI by mistake; this
spike has no QML UI at all.
- examples/common/CMakeLists.txt: gates the existing
MORPH_BUILD_QT/MORPH_BUILD_TESTS FATAL_ERROR checks and the
Qt6::WebSockets find_package() behind `if(NOT EMSCRIPTEN)` via an
early return (mirroring examples/bank/CMakeLists.txt's identical
pattern), so an Emscripten configure reaches wasm_spike/ instead of
aborting.
- examples/common/testkit/test_wasm_registration_path_native.cpp: the
CI-provable half. Proves natively that the WASM-safe registration
sequence (asyncRegistrationEnabled + setConnectHandler, no
waitForConnected) resolves correctly, plus a regression-guard test
documenting a real gap discovered while building this: calling
registerHandler() unconditionally right after constructing the
Bridge -- as originally drafted for this task -- never resolves,
because registerModelAsync() fails immediately with no
retry/queueing if the socket isn't connected yet, which it never is
at that exact point. main_wasm.cpp and this test both ship with the
corrected sequence (registerHandler() deferred into the
setConnectHandler callback, still fully WASM-safe).
- docs/findings/017-async-registration-fails-before-connect.md: files
this as a new blocker finding, since every prior test proving
asyncRegistrationEnabled=true "WASM-safe" (tests/qt/test_qt_websocket.cpp's
[issue26]) did so only after waitForConnected(), a call WASM must
never make -- so the ordering constraint this finding documents was
previously untested.
Emscripten (emcc/emcmake) was not available in this environment, so
the actual WASM compile gate (morph_ladder_wasm_spike) was never
exercised here -- only the native half. ladder_common_tests
(25 tests, up from 23) passes in full via
`cmake --build --preset gcc-debug --target ladder_common_tests &&
QT_QPA_PLATFORM=offscreen ctest --preset gcc-debug -L ladder`.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HffixfknmXdAjnM5hSGRBb
Code review of task 10 (WASM-remote spike) found two Important issues in the WASM-side code (never actually compiled in this environment, since Emscripten is unavailable here), both traced back to the plan's own draft: - examples/common/wasm_spike/CMakeLists.txt was missing a link to morph_qt_impl. morph::qt is header-only (INTERFACE); the compiled QtWebSocketBackend constructor/registerModelAsync/setConnectHandler bodies live in morph_qt_impl. main_wasm.cpp constructs a QtWebSocketBackend directly, so as written the WASM link would fail on undefined symbols -- every other real consumer in the repo (examples/common/CMakeLists.txt, tests/qt/CMakeLists.txt, tests/net_qt_interop/CMakeLists.txt) links both targets for the same reason. - main_wasm.cpp constructed its BridgeHandler<SpikeEchoModel> as a lambda-local inside the QTimer callback, right before calling execute(): the handler was destroyed the instant that lambda invocation returned, and ~BridgeHandler() deregisters the model, racing the still-in-flight server reply. Fixed by hoisting the handler into a std::optional<BridgeHandler<SpikeEchoModel>> that outlives both lambdas, constructed once inside the setConnectHandler callback (BridgeHandler's constructor performs the registration itself, so this also removes what would otherwise be a duplicate registration from the previously-separate bridge.registerHandler(binding) call). Emscripten remains unavailable in this environment, so these fixes are verified by inspection only, cross-checked against test_wasm_registration_path_native.cpp's and other established call sites' BridgeHandler construction/holding pattern -- not by an actual WASM compile. ladder_common_tests (25 tests) still passes in full; this diff does not touch the native test file. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HffixfknmXdAjnM5hSGRBb
…-safe
morph_ladder_gui went back to Qt6::Core only, as the plan's own constraint
and examples/TESTING.md's presenter rule 1 require: it now holds presenter.cpp
alone. AppContext, the one piece of shared gui/ code that genuinely needs
morph::qt/morph_qt_impl (and transitively Qt6::WebSockets) for its Remote
mode, moved into a new morph_ladder_app target (morph::ladder_app) that the
testkit also links.
AppContext's Remote branch no longer calls waitForConnected() and discards
the result — the exact WASM anti-pattern finding 017 and TESTING.md's "WASM
reality" both describe. It now builds the QtWebSocketBackend with
Config{.asyncRegistrationEnabled = true} and detects readiness through
setConnectHandler, mirroring wasm_spike/main_wasm.cpp. The new readiness
surface — ready() / onReady(callback) — lets a caller defer building its
presenters (and therefore its BridgeHandlers, which register) until the socket
is actually up; registering earlier fails permanently, with no retry, per
finding 017. Local mode is ready on construction and runs onReady inline.
BackendRig gained the accessors that make it composable with presenter code:
bridge(index), executor(), and url() (Socket only; std::logic_error
otherwise). Its Mode::Local also stopped delivering client callbacks on a
ThreadPoolExecutor thread — that raced pump.hpp's pumpUntil/awaitQt, which
read completion state from the Qt thread unsynchronized. A QtExecutor now
delivers callbacks in all three modes; the pool stays as LocalBackend's own
backing executor.
Along the way:
- pumpUntil/settle are [[nodiscard]] (a silently ignored timeout turns "never
completed" into "asserted on stale state"); the one discarding call site,
test_presenter.cpp, now REQUIREs the result.
- Presenter::track() runs finishOne() even when onOk throws, so a throwing
handler can no longer pin busy() true and hang every later settle().
- db_fixture.cpp/db_fault_fixture.cpp (one-line SPDX placeholders, both
headers being fully header-defined) are removed, matching the precedent set
for strand_interleaver.cpp.
- ladder_common_tests' discovered tests take a RESOURCE_LOCK: DbFixture resets
one shared on-disk database, which a future `ctest -j` would otherwise let
two test cases do to each other mid-test.
- The CMakeLists comment pointing at a gitignored .superpowers/ path is
replaced with the rationale inline; BackendRig's teardown-order comment now
describes the order the destructor actually runs.
- examples/CMakeLists.txt uses PROJECT_SOURCE_DIR, not CMAKE_SOURCE_DIR, so
an add_subdirectory()-embedded morph still finds morph_add_rung.cmake.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HffixfknmXdAjnM5hSGRBb
…labels Documentation and finding-ledger half of the rung-0 final-review fix wave. TESTING.md's db_fixture description said "per-fixture temp SQLite file (not bank's one-shared-DB pattern)"; what shipped is the opposite — one real on-disk database per test binary, reset between cases by dropping tables. The bullet now describes that, and says where isolation actually comes from (ctest's per-target working directory across binaries, the RESOURCE_LOCK within one). The presenter-architecture and build-wiring sections pick up AppContext's Remote readiness contract and the new third consumable target. Finding 004 is closed out: Task 8 landed the deterministic strand interleaver months after the fault proxy, but no step was assigned to drain the finding. Its `test:` field now names test_strand_interleaver.cpp alongside test_fault_proxy.cpp, and the body carries the resolution note it promised. Two new findings, both `open`, both from the whole-branch review: - 018: DbFaultFixture is SqlScopedLock-based, so it can only fault code that takes the same named advisory lock — never an ordinary DataMapper call. The SQLITE_BUSY / constraint-violation / rollback coverage TESTING.md and IMPLEMENTATION.md rule 5 promise is therefore not satisfiable as shipped. Deferred to whichever rung first needs store-error branch coverage (rung 1), since rung 0 ships no model. - 019: the testkit reaches into four morph detail:: namespaces with no public seam (async::CompletionState, exec::StrandExecutor/ModelId, bridge::HandlerBinding, model::defaultDispatcher/defaultRegistry), each with its call sites listed, framed against IMPLEMENTATION.md's rule-of-three promotion rule. The ladder-tests CI path filter missed src/qt/ — the compiled bodies of morph_qt_impl, the very thing the testkit conformance-tests and where finding 017's fix will land — plus the root CMakeLists.txt, cmake/, CMakePresets.json, and ci.yml itself. All are in the regex now. Also: the suite's ctest properties were being applied wrong. `LABELS "ladder;ladder-0"` is flattened by catch_discover_tests into two arguments, which shifted every following name/value pair by one — so neither the `ladder-0` label nor `TIMEOUT 120` was ever applied, and the RESOURCE_LOCK added in the previous commit silently wasn't either (`ctest -j8` still ran the suite concurrently). The call now passes one value per property name; a generated TEST_INCLUDE_FILES post-pass restores the rung label. Verified with `ctest --show-only=json-v1`, `ctest -L ladder-0` (28 tests, was 0) and `ctest -L ladder -j8` (now 5.3s, matching serial, was 1.5s). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HffixfknmXdAjnM5hSGRBb
Two trivial follow-ups flagged by the final whole-branch re-review: - app_context.cpp's connect-handler comment asserted the lifetime hazard couldn't happen; correct it to describe the real (currently unreachable, but real) danger the same way ~Bridge() documents it for its own reconnect handler, so it isn't copied verbatim into rung code as false reassurance. - The ladder-tests CI path filter didn't match examples/CMakeLists.txt itself, so a change to the ladder's own top-level build file (this branch touched it) wouldn't trigger the job meant to guard it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HffixfknmXdAjnM5hSGRBb
Everything that was described as running "nightly" (full ladder all modes, stress at scaled clients/actions, kanban TSan, all-rungs WASM compile, the Playwright browser smoke, the Windows compile-only build) now runs in the ordinary per-push/per-PR ladder-tests job instead of a separate off-hours schedule -- consistent with the rest of this repo's CI, which has no scheduled workflow today either. Weekly stays, narrowed to the one thing that genuinely can't run on every push: rung 8's load script, which needs a large, expensive runner. The demotion policy's "full matrix" outlet for exited rungs now points at weekly instead of nightly, so it still has somewhere to land without contradicting "no nightly." Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HffixfknmXdAjnM5hSGRBb
…quence guard Extracts pure, directly-testable decision functions out of BackendRig, FaultProxy, pump.hpp, and DbFixture (throwIfListenFailed/throwIfConnectFailed, computeDeadlineScale, computeConnectionString, decodeCallIdOrZero, isValidIncomingConnection) so previously process-bound or hard-to-trigger branches (env-var parsing, I/O-failure throws, undecodable reply frames) are covered by direct unit tests instead of chasing them through integration scenarios. Also drops db_fixture.hpp's sqlite_sequence skip-continue: reading Lightweight's own SqlSchema.cpp confirms ReadAllTables() already filters that table out before it reaches the fixture, so the guard could never execute. Adds matching test coverage for every extracted function plus a handful of previously-missing accessor/error-path cases (DbFaultFixture::lockName(), BackendRig::mode(), Presenter's onError/onReady(nullptr)/login() paths).
BRIDGE_MODEL_KEY(polls::PollModel, polls::OpenPoll, &polls::OpenPoll::pollId) is deferred to Task 5's poll_model.hpp, not added here: both docs/spec/core/shared_instances.md's worked example and the bank rung's account_model.hpp precedent place BRIDGE_MODEL_KEY beside the model's own BRIDGE_REGISTER_MODEL/BRIDGE_REGISTER_ACTION block, not in the DTO header, since PollModel's action registrations don't exist until Task 5.
…tring
Task 4's own report justified plain std::string for pollId/adminToken/
participantToken by claiming no sibling entity in this codebase uses
SqlAnsiString -- the scoped review found that claim false: bank and
pastebin both use it extensively for exactly this ID/token-shaped case
(bank's account number, pastebin's paste id), reserving plain
std::string for genuinely free-form Unicode text (bookmarks' titles/
notes), which is the correct convention this rung's own token fields
should have followed. Fixed to Light::SqlAnsiString<kTokenBytes>,
matching pastebin's exact assignment pattern
(Light::SqlAnsiString<N>{value}).
…t comment Task 6's own review found two things worth a direct fix: (1) PollOptionView's three Count fields had no default member initializers, so Task 6's fix for the empty-propagation vote-tally bug (Quantity's default state is empty, not zero, and arithmetic on it propagates empty forever) lived only at buildState()'s one call site -- any future construction site could silently reintroduce the identical bug with no compiler warning. Added default member initializers (Count::fromDouble(0.0)) to close the footgun at the type itself; PollOptionView stays an aggregate. (2) test_poll_model.cpp's comment on finalizePollDirectly() cited the wrong file as precedent for a direct mapper.Update() write -- test_bookmark_model.cpp only ever reads entities directly, never writes them; the real precedent is test_bookmarks_schema.cpp/test_polls_schema.cpp's own untransacted single-row Update() calls. Corrected the citation.
Review of Task 8 (77a8ac5) found two Important issues, fixed here: - No test exercised restoring a genuinely non-empty prior vote set -- all three existing UndoLastVoteChange tests only ever undid back to an empty vote. Added a test that submits, updates, then undoes, and asserts the restored (non-empty) tallies. - The restore write and the history-row cleanup ran in two separate transactions: applyVotes() committed the restore, then a second transaction deleted the leftover VoteHistoryRecord rows it had just written as a side effect. A crash between the two commits would leave votes restored AND a live, redo-able history row in place -- the exact "undo the undo" ping-pong the cleanup step exists to prevent. Fixed by giving applyVotes() a WriteHistory enum-class parameter (matching morph::model::Loggable's Yes/No convention) so the undo path's restore call never writes a spurious history row in the first place, plus an optional historyRowIdToDelete parameter so the one originally-consumed row is deleted inside applyVotes()'s own transaction. execute(UndoLastVoteChange) now makes exactly one call into exactly one transaction for the whole operation. Also: documented the finalized-poll Conflict case on execute(UndoLastVoteChange)'s @throws list, dropped the now-dead intermediate GetPollStateResult, corrected the header doc comment to describe the post-fix single-row deletion, and clarified in the README's Definition of done that "principal-scoped" means keyed on (pollId, participantName), not a framework-authenticated identity. Verified via a real compile+link+run against SQLite: full examples/polls suite now 36 test cases / 117 assertions, all passing (up from 35/113), including the interleaving and one-shot tests re-verified unaffected by the transaction refactor. Zero -Weverything warnings attributable to the touched files. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HffixfknmXdAjnM5hSGRBb
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HffixfknmXdAjnM5hSGRBb
morph_add_rung(NAME polls) does the standard target wiring, plus an explicit target_sources() for src/auth/ -- morph_add_rung() only globs src/models, src/db and src/app into ladder_polls_lib (cmake/morph_add_rung.cmake:91-92), so Tasks 1-10's polls_authorizer.cpp would otherwise fail to link (src/db/schema.cpp is already covered by the automatic glob; CMake dedups the identical path when it is also named explicitly). examples/CMakeLists.txt already listed "polls" in _morph_known_rungs (rung-0 build wiring, ce75cea) and the CI path-filter already matches examples/polls/ -- no change needed there. Real cmake --build build/clang-coverage --target ladder_polls_tests replaces every manual clang++ recipe Tasks 1-10 reconstructed from compile_commands.json. Zero warnings in polls' own code (src/, include/polls/, tests/) under -Weverything -- unlike rung 2's own CMakeLists.txt task, which fixed 43 pre-existing designated-field-initializer warnings, this rung's tests already write complete struct literals. Per-translation-unit -Werror verification against the real compile commands (Lightweight/unixodbc remapped to -isystem, -Wno-thread-safety-negative) hits two already-known, pre-existing, out-of-scope gaps: finding 028 (Lightweight/unixodbc headers not -Werror clean) and finding 029 (-Wthread-safety-negative on unannotated std::mutex under Clang 22) -- same shape as rung 2 hit, not new. A third, newly-discovered pre-existing gap in shared examples/common/testkit/backend_rig.hpp (-Wswitch-default on its exhaustive switch(mode), confirmed also present in bookmarks) is filed as finding 033 rather than fixed here -- shared testkit file, not this rung's to change unilaterally. 144/144 assertions in ladder_polls_tests (41 test cases); 1959/1959 assertions across the whole ladder (274 test cases: ladder-0 61, ladder-pastebin 51, ladder-bookmarks 121, ladder-polls 41) via ctest -L ladder. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HffixfknmXdAjnM5hSGRBb
…o self-contained Task 11's own scoped review found two small issues: (1) CMakeLists.txt explicitly re-listed src/db/schema.cpp in target_sources(), duplicating what morph_add_rung()'s own src/db/*.cpp glob already covers -- harmless (CMake deduplicates identical source paths before generating build rules, confirmed via a clean single-edge build.ninja inspection both before and after this fix) but the file's own comment contradicted the line immediately below it. Removed the redundant listing; only src/auth/polls_authorizer.cpp genuinely needs the explicit call. (2) finding 033's repro script omitted the Lightweight/unixodbc -isystem remap the prose already mentioned -- run literally as written, clang's default -ferror-limit=20 exhausts itself on unrelated finding-028-class warnings before ever reaching the real error. Added the remap and verified the corrected script reproduces exactly the one claimed error.
…d-attach tests Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HffixfknmXdAjnM5hSGRBb
Task 12's shared-instance-lifetime test hit the exact callId==0 bucket-sharing hazard finding 030 already documents (a fire-and-forget deregister's stray reply misrouted to a different parked sendSync call) -- but via a synchronous instances() call, not a synchronous register, and the test's own workaround comment described the mechanism accurately without naming the finding. Added the citation to the test, and recorded this as a third independent reproduction site in the finding itself: the hazard is general to any sendSync-based call competing for the shared bucket, not specific to registration -- strengthening the case for the finding's own "every sendSync call needs a real per-call callId" fix direction over a narrower register-only fix.
… 030 The scoped review of Task 12's finding-030 citation caught a real citation-accuracy defect in my own prior commit (1d2966b): the test comment attributed a claim to "the finding's own note" about QtWebSocketBackend::attachModel's empty-key path hitting the same callId==0 hazard -- a claim that is true (independently verified against src/qt/qt_websocket_backend.cpp:283-287, and previously surfaced during finding 030's original review) but was never actually written into the finding document itself. Adding it now as a genuine fourth reproduction site, and the most significant one: production code, not test code, taking the deregister-then-sendSync-register path any AllowShared handler resolves to when re-pointing to an unkeyed action.
…dline integration test Two new model-layer tests close out this rung's model-layer test work: - A poll's admin token does not finalize a different poll: PollModel is keyed per-poll, but this is written explicitly rather than assumed from the per-instance keying alone -- a bug in requireAdmin()'s poll-row lookup could silently pass a cross-poll admin token. - Bridge::setExecuteDeadline recovers a call the real rate limiter silently drops: a real QtWebSocketServerConfig::messagesPerSecond=5 server (via BackendRig's existing serverConfig parameter -- no hand-built server needed) drops 19 of 20 back-to-back SubmitVotes frames at the transport (confirmed independently via the server's own dispatchMessage log: only 1 of 20 SubmitVotes frames ever reaches the dispatcher); Bridge::setExecuteDeadline(500ms) recovers all 19 via ClientTimeoutError instead of hanging. First end-to-end proof (beyond the framework-prereqs plan's own unit tests) that the deadline mechanism and the rate limiter combine correctly in a real app -- the DoD's "run this rung's harness with messagesPerSecond configured ON" requirement. The cross-model rename-race analogue (rung 2's TagModel-renames-while- BookmarkModel-writes race) is considered and explicitly not applicable: this rung has only one model type (PollModel), so there is no second model to race a rename against. Skipped per the brief's own instruction, not silently omitted. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HffixfknmXdAjnM5hSGRBb
Two correctness bugs in this rung's framework-level deliverable, both found by review of the original implementation. Use-after-free (critical). The two callbacks pollOnce() hands to _dispatch captured a bare `this` with no liveness guard. The header claimed the `&_timer`-as-context-object trick covered the lifetime problem, but it does not: Bridge completes through QtExecutor::post -> QMetaObject::invokeMethod(..., Qt::QueuedConnection), so a pending completion callback is an event owned by QCoreApplication, not a connection owned by _timer -- destroying the poller cancels nothing. Destroying an EventPoller while busy() is true is the ordinary case (a user closes a poll view mid-tick), and it left a queued callback firing into freed memory. Adds a std::shared_ptr<const void> _liveness token, declared last so it is destroyed first, checked via weak_ptr as the first statement of both callbacks -- the same pattern Bridge itself uses (include/morph/core/bridge.hpp) and that backend_rig.hpp's QtDrivenMainThreadExecutor already documents for the identical hazard. Corrects the &_timer comment to say what it actually covers, and adds a note that member declaration order is load-bearing for both mechanisms. Success-callback ordering. _requestInFlight was cleared before the _onEvent fan-out and _lastEventId advanced after it, so throughout the caller's callbacks busy() already read false (a reentrant pollOnce() from a nested Qt event loop -- e.g. a modal dialog -- was not blocked) while the cursor still held its pre-batch value (so that reentrant tick refetched and reapplied the same batch, and this frame's later cursor write then rewound whatever the nested one had advanced to). The cursor now advances first and the flag is released last, via an RAII guard so a throwing _onEvent cannot wedge busy() at true forever -- the same rule gui/presenter.hpp's Presenter::track() already follows. Tests: three new regression cases, each verified to fail with its fix reverted. The use-after-free case destroys a heap-allocated poller with a condition-variable-gated dispatch genuinely outstanding, then pumps and asserts the event was never applied (without the guard it throws std::bad_function_call out of the freed _onEvent). Also takes releaseMutex around resetFeedControl()'s write to `released`, matching releaseBlockedCall()'s own write. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HffixfknmXdAjnM5hSGRBb
Recovery from a fatal error. _fatal was permanent once set -- start() refused to rearm, copy and move were both deleted -- so a caller's only way back was destroying and reconstructing the poller, which also re-runs the constructor's bridge.setExecuteDeadline() and clobbers whatever deadline anything else on that Bridge had set since. That is at odds with this rung's own design intent, where a stale-cursor client falls back to GetPollState and resyncs. resume(newCursor) clears the fatal state, repoints the cursor, and rearms the timer. Production-wiring recipe. The class comment told whoever wires this up next to build a Dispatch closure that forwards PollPresenter's eventsReceived/failed signals into onSuccess/onError. That is unsound: failed(QString) is one signal shared by all nine PollModel actions, and a live poll view has submitVotes/addComment/finalizePoll potentially in flight alongside poll ticks, so such a closure cannot tell whose failure it just saw -- it stops the poller for an unrelated action's error while this tick's real failure goes unreported. reportError also catches only std::exception, so a non-std::exception failure emits nothing at all and wedges _requestInFlight forever. Replaced with a @warning saying so plainly, plus the pattern that does work: build Dispatch directly over a per-call BridgeHandler completion, as test_event_poller.cpp's makeDispatch() already does, which keeps ClientTimeoutError a real catchable type and makes cross-attribution impossible. No PollPresenter change -- documentation only. Minor corrections: isClientTimeout's comment no longer claims to detect a *wrapped* ClientTimeoutError (it does one rethrow-and-catch, no nested walk); handleError's _fatal guard is documented as load-bearing rather than unreachable, since nothing mechanically enforces Dispatch's call-exactly-once contract; and the _onFatalError call site records why it is safe for that callback to destroy the poller, so a future refactor does not move a member access after it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HffixfknmXdAjnM5hSGRBb
Task 16: PollFormsController (one BridgeHandler<PollModel, AllowShared> shared by every already-open-poll action -- OpenPoll/GetPollState/ AddComment/FinalizePoll/UndoLastVoteChange/SubmitVotes/UpdateVotes/ GetEventsSince), PollBridge (the QML-facing adapter wrapping both PollFormsController and PollPresenter's createPoll), and Main/CreatePollView/ VoteView.qml. Three actions are schema-driven (AddComment, FinalizePoll, UndoLastVoteChange); CreatePoll::options and SubmitVotes/UpdateVotes::votes hit finding 031 (DynamicForm has no array-field control) and are driven by hand-written QML pickers instead, mirroring rung 2's BulkEdit workaround. Task 15's EventPoller is wired to a real view via PollBridge's Dispatch closure over PollFormsController::getEventsSince -- never PollPresenter's shared failed(QString) signal, per that class's own doc comment. Found and worked around along the way: BridgeHandler::executeJson silently skips the payload-keyed attach step on an AllowShared handler (finding 034), since its registered executor closes over the plain (NoSharing) execute<>() overload regardless of the real handler's Sharing argument. OpenPoll is therefore dispatched only via PollFormsController's own typed method, never through the generic schema/executeJson path. 67 polls tests pass (17 new: offscreen QML smoke test for all three views, adapter-layer suite with QMetaObject surface assertions plus one real end-to-end EventPoller tick); 307/307 across the whole ladder. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HffixfknmXdAjnM5hSGRBb
…attach
main_wasm.cpp mirrors bookmarks'/pastebin's Remote-only WASM shell, with
three genuinely new things: (1) it dispatches OpenPoll{pollId} through
PollFormsController's AllowShared handler, the first real WASM exercise of
Bridge::attachHandlerAsync's async keyed-attach branch; (2) Main.qml's
nativeClient initial property is set to false, so CreatePollView (native-only
per this rung's Global Constraints) has no reachable UI path; (3) a small
EM_JS shim reads a `?poll=<id>` query parameter so a participant can land
directly on VoteView without the organizer-only CreatePoll flow -- wired
through a new, empty-by-default Main.qml `initialPollId` property, and a
new MORPH_LADDER_POLLS_WASM_SERVER_URL compile definition in
examples/polls/CMakeLists.txt mirroring the sibling rungs' own WASM-url
wiring.
Verified locally: a native syntax-only compile against a Qt-shaped
compile_commands.json entry (with a minimal EM_JS stub, since no Emscripten
toolchain exists in this environment), and the full `ctest -L ladder`
suite (307/307, including the polls QML smoke test against the changed
Main.qml). The actual Emscripten compile remains CI-only, per
.github/workflows/wasm-ladder.yml's new ladder_polls_gui_wasm target.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HffixfknmXdAjnM5hSGRBb
The final whole-branch review's I3: four DTO fields violated
examples/IMPLEMENTATION.md rule 3, which forbids bare `bool`/raw types in
DTO fields ("a two-state flag is a two-enumerator `enum class`") and
requires "a named opaque newtype per role ... never a loose `std::string`"
for capability tokens. Rungs 1 and 2 have zero bare-bool DTO fields; this
rung shipped two, while inconsistently using `enum class WriteHistory` for
an internal helper parameter in the same file family.
- `GetPollStateResult::finalized` -> `polls::Finalized{No,Yes}`
- `UndoLastVoteChangeResult::restored` -> `polls::Restored{No,Yes}`
Both reflected with `glz::enumerate` exactly like `pastebin::Visibility`
and `bookmarks::ReadState`, so the wire form is `"Yes"`/`"No"`, not an
ordinal (a bare ordinal also degrades the schema writer's `$defs` entry).
- `CreatePollResult::adminToken`/`participantToken` -> `polls::AdminToken`/
`polls::ParticipantToken`, opaque newtypes shaped exactly like
`bookmarks::AuthToken` (optional payload, `hasValue()`, `operator*`,
`<=>`, payload-only `glz::meta`). Two distinct types, not one: an admin
token can no longer be passed where a participant token is meant, which
is why `PollModel::requireAdmin` now takes `const AdminToken&`.
Every call site follows: `poll_model.cpp`'s construction and
`requireAdmin` call, `poll_qml_bridges.cpp`'s QVariantMap projections (the
QML-facing map still carries a plain bool/string -- QML has no enum class),
and six test files, including the two payload assertions that now read
`"restored":"Yes"`/`"finalized":"Yes"`.
Full ladder suite: 307/307.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HffixfknmXdAjnM5hSGRBb
… WASM The final whole-branch review's I1, confirmed real by reading the chain it cites: `EventPoller`'s constructor calls `Bridge::setExecuteDeadline` unconditionally (event_poller.hpp), that lazily constructs a `TimeoutScheduler` (bridge.hpp), and that constructor spawned a `std::thread` -- while the ladder's WASM clients are built against `wasm_singlethread` Qt (.github/workflows/wasm-ladder.yml) with no `-pthread` anywhere in cmake/morph_add_rung.cmake. Emscripten's non-pthread `pthread_create` stub fails, so libc++ throws `std::system_error` from that constructor -- on every successful poll open in a browser tab, since VoteView.qml's `Component.onCompleted` calls `openPoll` and its `.then` constructs the poller. The `ladder-wasm` gate is compile-only and cannot catch it. Fixed rather than degraded: under `__EMSCRIPTEN__ && !__EMSCRIPTEN_PTHREADS__` the same public API (`schedule`/`cancel`/`Handle`) is built on `emscripten_async_call` -- the browser's `setTimeout` -- and fires on the main thread, which is where the Qt event loop and every `QtExecutor`-posted completion callback already run. Deadlines still fire; `ClientTimeoutError` still races the real reply. Two documented behavioural differences: callbacks are never concurrent with the caller, and `cancel()` releases the callback immediately but lets the underlying browser timer elapse harmlessly instead of clearing it. Pending timers hold a `weak_ptr` to the scheduler's state, so one that outlives its scheduler returns without touching freed storage. Honest scope: no Emscripten toolchain exists in this repository, so neither the original hazard nor this fix has been observed on a real WASM build -- stated as such in the header, in docs/spec/core/completion.md and in event_poller.hpp. What *was* verified locally: the browser branch compiles warning-free under a stubbed `emscripten.h` with `-D__EMSCRIPTEN__`, and against a queued stub it fires once, honours `cancel()`, and drops pending timers safely when the scheduler is destroyed. Also in event_poller.hpp (the review's M6, M7 and two parked Task-15 nits): `@tparam` tags moved from the `@file` block onto the class template itself (silencing the branch's one -Wdocumentation warning), `handleError` now documents that `_onFatalError` is itself a member whose frame a self-destroying callback would free, `lastEventId()` reflects the cursor advancing before the fan-out, and `ApplyEvent` warns that `onEvent` must not destroy the poller. Full ladder suite: 307/307. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HffixfknmXdAjnM5hSGRBb
…story
The final whole-branch review's I2: `PollModel::requireParticipant()` had
zero call sites anywhere, yet three doc sites (`poll_model.hpp`,
`polls_authorizer.hpp`, README design decision 1) claimed "every later
admin/participant-gated action reuses them". In the shipped code only
`FinalizePoll` checks anything at all.
Option (b) of the two the review offered: correct the documentation rather
than wire the check up. Wiring it up would contradict this rung's own
design decision 2 ("attaching to a poll by id is meant to be as open as
knowing the link"), would gate actions no client can present a token for
(`VoteView.qml` has an admin-token field and nothing else), and would add
no authority in any case -- one participant token is minted per poll, not
per participant, so every voter would present the same secret while
`pollId` is already 128 bits of `std::random_device` entropy.
- `requireParticipant()` removed from both header and `.cpp` (dead code
that implied a check which does not happen).
- `poll_model.hpp` gains a "What is actually gated, stated exactly"
section naming `FinalizePoll` as the only token-gated action and every
ungated one explicitly, plus why `participantToken` is generated,
returned, displayed and verified by nothing.
- `polls_authorizer.hpp`'s `@file` comment drops the same claim, and (the
review's parked Task-7 nit) stops implying it mirrors
`BookmarksAuthorizer`'s shape: they share one conclusion about two
finding-027-limited hooks and nothing else -- bookmarks derives from
`SigningAuthorizer` with real token verification and inline bodies,
polls derives from `AllowAllAuthorizer` and splits a `.cpp` for two
`return true;` lines.
- README design decision 1 gains the same exact statement.
No behaviour change; full ladder suite 307/307.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HffixfknmXdAjnM5hSGRBb
…requisites The final whole-branch review's I4: findings 001 and 002 still read `disposition: open` with bodies asserting the fix does not exist, while this branch's two framework-prerequisite tasks built exactly what they asked for. Closed following finding 004's established convention -- a `**Resolution (...)**` section citing the real symbols and their current file:line, the delivered tests moved into the `test:` field, and a `**Closed.**` paragraph noting that the disposition stays `fix-scheduled` only because examples/FINDINGS.md defines no `closed` value. - 001: `registerModelSharedAsync` (backend.hpp:187) / `attachModelAsync` (backend.hpp:286), preferred by Bridge at bridge.hpp:576/468, covered by tests/test_async_registration.cpp and tests/qt/test_qt_websocket.cpp's `[issue26][shared-instances]` cases. The WASM half stays honestly caveated: compile gate only, no Emscripten toolchain here. - 002: `Bridge::setExecuteDeadline` (bridge.hpp:821) and `morph::backend::ClientTimeoutError` (backend.hpp:475), covered by tests/test_client_execute_deadline.cpp -- and, as that finding predicted, with no `Completion`/`CompletionState` API change. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HffixfknmXdAjnM5hSGRBb
… ships The final whole-branch review's I5, plus its parked Task-4 nit. - README status flips from "planned" to "shipped", matching rungs 1 and 2's wording and pointing at the two sections that say what that does and does not mean. - The known-gap bullet claiming "there is no `gui_wasm/main_wasm.cpp` in this rung at all yet" is replaced: Task 18 wrote it. In its place, three honest bullets -- the browser client cannot create polls (by design), no native desktop entry point exists at all so nothing here has been run as an application, and `setExecuteDeadline`'s WASM hazard plus its compile-gate-only fix. - The Definition-of-done live-demo bullet now plainly states it is **not satisfied**, and why, matching the "Confirmed (Task N)" style of the bullets beside it; the kanban-reuse bullet gets the "Confirmed (Task 15)" annotation it was missing. - `Main.qml` stops attributing its controller property to a `gui/main.cpp` that does not exist, and stops calling `gui_wasm/main_wasm.cpp` "a future" file now that it is real. - `polls/db/db_model.hpp` gains the `@file` comment pointing at finding 025's WASM header-vs-link rationale, which pastebin's and bookmarks' own copies of this mixin both carry. No `gui/main.cpp` was written: the review's own recommendation was to document the gap, not close it in a fix round. Full ladder suite: 307/307. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HffixfknmXdAjnM5hSGRBb
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds the application ladder — a planned sequence of eight stateful example applications of gradually increasing complexity, each anchored to an existing open source project and sequenced so every morph subsystem (strands, shared instances, journal, offline queue, forms/exact values, sessions/authorization, remote transport) is stressed by at least two rungs — and now also rung 0: the shared infrastructure every later rung builds on.
The plan was hardened by seven independent review rounds (general edge cases, adversarial concurrency, GUI test architecture, delivery realism, forms/units capability mapping, a verification pass, and a final holistic program review) before any rung-0 code was written. Their corrections are folded into the design docs: framework prerequisites are scheduled as issues rather than rung discoveries; known limits (no server push, WASM constraints, journal replay semantics, exactly-once delivery) are stated per rung with required tests; and
examples/TESTING.mdestablishes the binding convention for unit testing every rung's GUI in both deployment modes.The holistic round restructured the program: the committed build scope is rungs 0–4 plus five no-app spikes; rungs 5–8 are a design annex (their READMEs are the deliverable; construction is a post-rung-4 decision). The program's product is findings fixed, not apps shipped —
examples/FINDINGS.mddefines the finding pipeline, fix budget, exit criteria, and CI demotion policy.Ladder planning & design docs
examples/LADDER.md— the ladder overview: rung table, cross-cutting stress map, six recurring strains, rung 0 / scope / sequencing, framework prerequisites, operations & security conventions, and the journal-honesty position.examples/FINDINGS.md— the finding pipeline: what counts as a finding (a minimal failing test, or a spec-cited impossibility), triage dispositions, the fix-time budget, rung exit criteria (feature completeness explicitly is not one), and the demotion policy so harvested rungs never tax framework PRs.examples/IMPLEMENTATION.md— binding implementation rules: models are the application; GUIs stay minimal and schema-driven; DTO fields use strong types exclusively; persistence exclusively through the Lightweight ORM; models 100% unit tested; per-rung PR checklist.examples/TESTING.md— dual-mode GUI testing strategy, multi-client stress harness, testkit component spec with per-rung ordering, fault-injection proxy, WASM reality check, build-system and CI tiering.examples/{pastebin,bookmarks,polls,kanban,ledger,lims,crm,forge}/README.md— one per rung: implementation scope, open source references to study, morph subsystems exercised, expected strain points, required tests, and design questions to resolve in writing.Rung 0: shared infrastructure (this PR's code)
No application exists yet — rung 0 is pure plumbing that rung 1 (pastebin) and every later rung build on.
Findings backfill (
docs/findings/001–019, perFINDINGS.md's "back-fill is the first task of rung 0" mandate): known framework gaps found while building this infrastructure, each a minimal failing test or a spec-cited impossibility. Two are genuine, previously-undocumented discoveries this rung's own construction surfaced:QtWebSocketBackend::registerModelAsync()fails permanently (no retry, no queueing) if called before the socket finishes connecting — found while building the WASM-remote spike, and the reason every prior async-registration test had missed it (they all calledwaitForConnected()first, which is itself forbidden on WASM).Build wiring:
MORPH_BUILD_LADDERCMake option,examples/CMakeLists.txt,cmake/morph_add_rung.cmake(the per-rung target scaffolding function, ready for rung 1), andexamples/common/CMakeLists.txtdeclaring the shared link targets.Shared testkit (
examples/common/testkit/):pump.hpp— the sanctioned async wait/pump surface (pumpUntil/awaitQt/settle) every test in the ladder uses instead of sleeping.db_fixture.hpp/db_fault_fixture.hpp— real on-disk SQLite fixtures mirroring the vendored Lightweight ORM's own test conventions (a shared database reset by dropping tables, and genuine cross-session lock contention viaSqlScopedLock) rather than a mock.backend_rig.hpp— a three-mode (Local/LocalSingleThread/Socket) test fixture so one test body runs against every deployment shape.fault_proxy.hpp/.cpp— an in-process WebSocket relay with scriptable drop/delay/duplicate/kill rules, keyed precisely on a call's id (via a race-free request-observer hook), for exactly-once and reconnect-mid-replay testing.strand_interleaver.hpp— aDeterministicExecutorthat lets a test script an exact task interleaving instead of depending on OS thread scheduling.Shared presenter architecture (
examples/common/gui/,examples/common/wasm_spike/):Presenter— abusy()/idle()completion-tracking base every rung's GUI presenters derive from.AppContext— a backend-parameterized context (Local{workers}/Remote{url}) replacing bank's hard-wiredLocalBackend; itsRemotemode is WASM-safe by construction (asyncRegistrationEnabled+setConnectHandler+ aready()/onReady()contract), split into its ownmorph_ladder_apptarget so the Qt-Core-onlymorph_ladder_guitarget stays free ofQt6::WebSockets.wasm_spike/— a minimal WASM client provingQtWebSocketBackendworks from an Emscripten build (never previously exercised in this repo; bank's own WASM GUI is local-only), plus the native-side proof test that's actually verified in CI.CI: a new
ladder-testsjob in.github/workflows/ci.yml, path-filtered so it only runs when ladder-relevant files change.Testing
ctest --preset gcc-debug), including all 28ladder-labeled tests.sqlite3_*calls, nosleep_foroutsidepump.hpp, anywhere in the new code.🤖 Generated with Claude Code
https://claude.ai/code/session_01HffixfknmXdAjnM5hSGRBb