Skip to content

ladder: rung 3 -- polls - #91

Merged
Yaraslaut merged 4 commits into
masterfrom
ladder-polls
Aug 16, 2026
Merged

ladder: rung 3 -- polls#91
Yaraslaut merged 4 commits into
masterfrom
ladder-polls

Conversation

@Yaraslaut

Copy link
Copy Markdown
Member

What

Rung 3 of the application ladder: polls, anchored on Rallly. Split out of #41 into its own PR, targeting #88's ladder-foundation branch since it depends on the shared testkit/CMake infra there.

Draft until #88 merges — retarget to master and un-draft once it does.

Contents

examples/polls/ in full: PollModel (this ladder's BRIDGE_MODEL_KEY-shared/keyed model), DTOs, entity, schema migration, authorizer, server, Qt/QML GUI, and tests — 40 files.

Includes a fix folded in during review of the combined branch:

  • PollModel acquires connections from Lightweight::GlobalDataMapperPool() per execute() call rather than holding one for its own lifetime (WithMapper removed); applyVotes() (a private helper called from within execute() bodies that don't need to share its connection) makes its own independent acquisition.

Verification

Configured and built standalone against the ladder-foundation base with -DMORPH_LADDER_RUNGS=polls (no other rung present). Full suite passes: 557 assertions in 68 test cases (SQLite default). Spec-citation and test-type-name lints clean.

@codecov

codecov Bot commented Aug 14, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Split out of application-ladder (originally bundled with pastebin/
bookmarks and the shared foundation in #41) into its own PR against the
rung-0 foundation (#88). Includes the pool-migration fix folded in
during review of the combined branch:

- PollModel (this ladder's BRIDGE_MODEL_KEY-shared/keyed model) acquires
  connections from Lightweight::GlobalDataMapperPool() per execute()
  call rather than holding one for its own lifetime (WithMapper
  removed); applyVotes() (private helper, called from within execute()
  bodies that don't need to share its connection) makes its own
  independent acquisition.

Verified standalone against the ladder-foundation base: configures and
builds with -DMORPH_LADDER_RUNGS=polls and no other rung present. Full
suite passes: 557 assertions in 68 test cases (SQLite default).
Spec-citation and test-type-name lints clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@Yaraslaut
Yaraslaut changed the base branch from ladder-foundation to master August 16, 2026 11:27
@Yaraslaut
Yaraslaut marked this pull request as ready for review August 16, 2026 11:36
Yaraslau Tamashevich and others added 2 commits August 16, 2026 14:41
…tring types

Per docs/superpowers/specs/2026-08-11-strong-storage-types-design.md
item 3's polls inventory (spec file since removed from the tree by
2851376, recovered from git history for this change).

poll_entity.hpp:
- PollRecord::title -> Light::SqlAnsiString<kMaxTitleBytes> (200)
- OptionRecord::label -> Light::SqlAnsiString<kMaxOptionLabelBytes> (100)
- VoteRecord/CommentRecord/VoteHistoryRecord::participantName ->
  Light::SqlAnsiString<kMaxParticipantNameBytes> (80)
- CommentRecord::body -> Light::SqlAnsiString<kMaxCommentBytes> (500)
- VoteHistoryRecord::previousVotesJson -> Light::SqlMaxDynamicAnsiString
  (serialized JSON, unbounded)
- PollEventRecord::kind -> Light::SqlAnsiString<32> (short internal tag,
  no existing DTO constant)
- PollEventRecord::summary -> Light::SqlMaxDynamicAnsiString (free text,
  no existing bound)

pollId/adminToken/participantToken are untouched -- already
SqlAnsiString<kTokenBytes> from an earlier pass.

poll_model.cpp: added a templated textOf(SqlAnsiString<N>) plus a
SqlMaxDynamicAnsiString overload (mirroring pastebin::PasteModel's
textOf() precedent) and wrapped every entity->DTO read of these fields
in it; DTO->entity writes needed no changes since Light::Field's
assignment operator already accepts anything constructible into the
field's value type. Added five static_asserts pinning each bounded
field's SqlAnsiString capacity to its DTO-level kMax*Bytes constant
(matching pastebin's kMaxSyntaxBytes static_assert), so title/label/
participantName/body's storage capacity and validate() bound can never
drift apart silently. kind/summary have no DTO constant to pin against
and are left unasserted, per the spec's own note on those two fields.

schema.cpp: poll_events.kind's DDL widens from Varchar(16) to Varchar(32)
and summary from Varchar(200) to Text(), to match the entity's new
capacities (kind's entity comment already documented 32 as the chosen
size; summary is now genuinely unbounded like previous_votes_json's
existing Text() column).

test_polls_schema.cpp and every other polls test file needed no changes:
all *Record field assignments in these tests use string literals, which
construct implicitly into SqlAnsiString<N>/SqlMaxDynamicAnsiString the
same way they did into plain std::string, and its two Field::Value()
comparisons against string literals resolve through SqlFixedString's
operator==(string_view) overload.

Verified via the existing build/polls tree (MORPH_LADDER_RUNGS=polls,
MSVC/Ninja, Debug), reconfigured and rebuilt incrementally: full rebuild
succeeded and all 68 ladder-polls-labeled ctest cases pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds “polls” (application ladder rung 3) as a full end-to-end example app: a shared/keyed PollModel backed by SQLite/Lightweight, a standalone server, Qt/QML GUI (including WASM client), and a comprehensive Catch2 test suite exercising shared-instance lifecycle, DTO validation, schema, presenter/bridge wiring, and event polling.

Changes:

  • Introduces the polls domain model (PollModel) keyed by pollId, including DTOs, entities, migration, and authorization shape.
  • Adds runnable surfaces: standalone server (ladder_polls_server), QML module + GUI adapter layer, and a WASM GUI entry point.
  • Adds extensive unit/integration tests covering DB schema, shared-instance behavior, presenter + QML bridge wiring, and GUI smoke loading.

Reviewed changes

Copilot reviewed 40 out of 40 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
examples/polls/CMakeLists.txt Rung build wiring and WASM server URL define.
examples/polls/gui/qml/Main.qml Main application shell + landing + navigation.
examples/polls/gui/qml/CreatePollView.qml Organizer create-poll screen (hand-rolled options editor).
examples/polls/gui/qml/VoteView.qml Vote screen, schema-driven actions, and live event log display.
examples/polls/gui_lib/poll_forms_controller.hpp Single shared-handler controller for poll actions + schema allow-list.
examples/polls/gui_lib/poll_forms_controller.cpp Controller method implementations over one AllowShared handler.
examples/polls/gui_lib/poll_presenter.hpp Presenter wiring over two handlers (creator + shared/attached handler).
examples/polls/gui_lib/poll_presenter.cpp Presenter action routing and error reporting.
examples/polls/gui_lib/poll_qml_bridges.hpp QML-facing adapter (PollBridge) + event poller wiring.
examples/polls/gui_lib/poll_qml_bridges.cpp DTO↔QVariant conversion + bridge method implementations.
examples/polls/gui_lib/poll_schemas.hpp Schema JSON for schema-driven actions.
examples/polls/gui_wasm/main_wasm.cpp WASM client entry: remote context, URL ?poll= parsing, initial props.
examples/polls/include/polls/app/app.hpp Server-side bootstrap wrapper (RemoteServer + action log + limits).
examples/polls/src/app/app.cpp App implementation + limit policy + action log lifecycle.
examples/polls/include/polls/auth/polls_authorizer.hpp Unconditionally permissive authorizer (per rung design).
examples/polls/src/auth/polls_authorizer.cpp Authorizer hook implementations.
examples/polls/include/polls/core/errors.hpp Domain exception hierarchy for polls.
examples/polls/include/polls/core/types.hpp Strong ID types, vote choice enum, and shared constants.
examples/polls/include/polls/db/database.hpp DB bootstrap API (setup) for production server.
examples/polls/src/db/schema.cpp Lightweight migration + schema creation.
examples/polls/include/polls/db/poll_entity.hpp Lightweight entity records (poll/options/votes/comments/history/events).
examples/polls/include/polls/dto/poll_dto.hpp Poll actions/results DTOs + validation + glaze metadata.
examples/polls/include/polls/dto/vote_dto.hpp Vote/comment/finalize/undo DTOs + validation helpers.
examples/polls/include/polls/dto/event_dto.hpp Event log DTOs + cursor validation.
examples/polls/include/polls/models/poll_model.hpp PollModel public API + bridge registration + keying.
examples/polls/include/polls/units.hpp Unit system + Count quantity for tallies.
examples/polls/src/server/main.cpp Standalone server process bootstrap + env config + graceful shutdown.
examples/polls/tests/test_app.cpp App boot smoke test via simulated remote backend.
examples/polls/tests/test_gui_qml_smoke.cpp Offscreen QML engine-load smoke tests.
examples/polls/tests/test_poll_dto.cpp DTO validation + result structure tests.
examples/polls/tests/test_vote_event_dto.cpp Vote/event DTO validation tests.
examples/polls/tests/test_polls_types.cpp Strong-id / error-type / constant sanity tests.
examples/polls/tests/test_polls_schema.cpp Schema migration + entity round-trip + unique index enforcement tests.
examples/polls/tests/test_polls_authorizer.cpp Authorizer “admits everything” contract tests.
examples/polls/tests/test_poll_presenter.cpp Presenter action wiring across backend-mode matrix.
examples/polls/tests/test_poll_qml_bridges.cpp QML-bridge surface and end-to-end adapter wiring tests.
examples/polls/tests/test_shared_instance_lifecycle.cpp Shared-instance attach/lifetime/poisoning + rate-limit deadline recovery tests.
Suppressed comments (3)

examples/polls/gui/qml/VoteView.qml:204

  • Same issue as above: this updates picks on both the checked and unchecked transitions. Only update when checked is true so the selected RadioButton is the one that persists.
                                text: "If need be"
                                enabled: page.state && !page.state.finalized
                                ButtonGroup.group: choiceGroup
                                checked: page.pickFor(optionRow.modelData.id) === "IfNeedBe"
                                onToggled: page.setPick(optionRow.modelData.id, "IfNeedBe")

examples/polls/gui/qml/VoteView.qml:211

  • Same issue as above: toggled fires when unchecking too, so this can overwrite the selection after the user picked a different value. Update picks only when the button becomes checked.
                                text: "No"
                                enabled: page.state && !page.state.finalized
                                ButtonGroup.group: choiceGroup
                                checked: page.pickFor(optionRow.modelData.id) === "No"
                                onToggled: page.setPick(optionRow.modelData.id, "No")

examples/polls/include/polls/core/types.hpp:61

  • Same documentation issue as OptionId: PollEventId::operator*() cannot be UB — it just returns the stored integer. Adjust the comment to match the real contract (0 means “not entered”; check hasValue() when needed).
    /// @brief Unchecked access to the engaged value (UB when empty, exactly
    ///        like `std::optional::operator*`).
    /// @return The engaged value.
    [[nodiscard]] constexpr std::int64_t operator*() const { return value; }

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread examples/polls/gui/qml/VoteView.qml Outdated
Comment thread examples/polls/include/polls/core/types.hpp Outdated
Comment thread examples/polls/src/server/main.cpp
cpp-review finding:
- schema.cpp: previous_votes_json/summary use NVarchar(0), not Text() --
  both back a SqlMaxDynamicAnsiString entity field (genuinely unbounded),
  and Text() encodes no such contract; matches pastebin's own NVarchar(0)
  precedent for unbounded storage and the fix already applied to
  bookmarks' equivalent Text()-vs-entity-capacity mismatch (PR #90)

Copilot findings:
- VoteView.qml: each vote RadioButton's onToggled fired unconditionally,
  including on the checked->unchecked transition Qt's shared ButtonGroup
  triggers on the sibling that just lost the selection -- so clicking one
  choice could have its own setPick() call overwritten right back by the
  previously-checked button's own unconditional handler, depending on
  which of the two `toggled` signals QML fires second. Guarded all three
  (Yes/If need be/No) on `if (checked)`.
- types.hpp: OptionId/PollEventId::operator*()'s doc comment claimed "UB
  when empty, exactly like std::optional::operator*" -- both wrap a plain
  std::int64_t with 0 as their own sentinel, not a std::optional, so
  reading either when "empty" just returns 0, never UB. Rewrote both
  comments to state the real contract (Copilot's suppressed-comments list
  named PollEventId only; OptionId carries the identical copy-pasted
  comment one type up in the same file, so fixed both).

Verified: full ladder_polls_tests suite (68 cases) passes against SQLite;
the 1 unrelated ctest-reported failure (QtWebSocketBackend concurrent
dispatch, untouched by this diff) is the same console em-dash encoding
artifact in ctest's -R/-I re-invocation seen on PR #90, not a real
failure.
@Yaraslaut
Yaraslaut merged commit d423e0e into master Aug 16, 2026
24 checks passed
Yaraslaut pushed a commit that referenced this pull request Aug 17, 2026
Resolves examples/kanban/README.md's design questions in writing, per
the ladder's own discipline rule. Covers steps 1-5+7 (steps 6/8 stay
deferred, per the README's own scoping):

- Exactly-once (MoveTaskPosition): generalizes bookmarks::ImportBookmarks'
  client-op-id + server-side applied-ops-ledger pattern, storing the
  full serialized GetBoardResult (not a placeholder) so a replaying
  client reconciles against the real outcome.
- Strand ordering / WIP limits / position renumbering: rely entirely on
  the framework's existing strand-per-instance guarantee (verified
  against docs/spec/core/shared_instances.md); no new locking.
- Per-project RBAC: in-model requireRole() check mirroring
  PollModel::requireAdmin(), not an IAuthorizer interface change --
  resolved via an analysis agent, grounded in shared_instances.md's and
  security.md's own already-written positions.
- Activity stream: derived from IActionLog::entries(entityKey) -- no
  new storage.
- Offline: composes SqliteOfflineQueue/SyncWorker/ReconnectCoordinator
  as-is; verified (not assumed) that a reconnect flap cannot preempt an
  in-progress replay.

Two testkit-scope findings, verified by file-existence checks:
- action_driver.hpp/process_pool.hpp/offline_rig.hpp are rung 4's own
  obligation per examples/TESTING.md's ownership table (confirmed none
  exist yet).
- client_pool.hpp/convergence.hpp were TESTING.md's documented rung-3
  obligation but polls (merged, PR #91) never built them -- absorbed
  into this rung's scope since kanban's own convergence DoD item needs
  them regardless of original ownership.

Framework gap filed and cross-referenced: morph#112 (IOfflineQueue has
no depth bound or overflow policy), verified against
offline_queue.hpp/sqlite_offline_queue.hpp/file_offline_queue.hpp.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants