Skip to content

kanban: render the remaining forms, and file the gap that stops the last two - #389

Merged
Yaraslaut merged 1 commit into
masterfrom
fix/344-kanban-remaining-forms
Sep 1, 2026
Merged

kanban: render the remaining forms, and file the gap that stops the last two#389
Yaraslaut merged 1 commit into
masterfrom
fix/344-kanban-remaining-forms

Conversation

@Yaraslaut

Copy link
Copy Markdown
Member

Closes #344. Follows #357, which rendered the login form; this does the rest of the rung.

examples/LADDER.md designates kanban the single polished showcase, and examples/IMPLEMENTATION.md's rule 2 makes hand-built input widgets forbidden by default, allowing two justifications — (a) the generated UI cannot express the interaction, or (b) pure glue — each of which must be written in the rung README. The rung had 26 hand-written controls across seven QML files, no morph::forms code at all, and neither justification written. Triage had already refuted (a) for the enumerated inputs by compiling schemaJson<A>() against the rung's real DTOs.

Converted — five forms, on top of #357's login

Form Action Screen
Create project CreateProject ProjectListView.qml
Add column CreateColumn BoardView.qml
Add swimlane CreateSwimlane BoardView.qml
New task CreateTask BoardView.qml, one per column
Add comment AddComment TaskDetailPopup.qml

All five render from schemaJson<A>() through the shipped MorphForms DynamicForm, with the renderer's own Submit button.

BoardBridge gained schemasJson/submitIfValid/replyReceived — the contract ProjectAdminBridge already had — forwarding to a new BoardPresenter::submitForm that dispatches through executeJson and decodes the reply, so boardOpened/commentAdded still fire. Without the decode the board property would only catch up on the next poll tick, which is a visible regression rather than a refactor. ProjectAdminPresenter::submitForm became the two-model routing table. CreateTask's column/swimlane ids and AddComment's task id are hidden in fieldMetadata and seeded by the owning view; CreateColumn::wipLimit joined optionalFields.

Left hand-built, with the justification written

The drag-and-drop board (MoveTaskPosition) — rule 2(a), no issue filed. Its schema renders fine; the interaction is the problem. The destination column, swimlane and position are produced by the gestureDropArea.onDropped computes them from geometry — so a schema-rendered form has no input left to collect. The README says that, rather than dressing a gesture up as a subsystem gap.

MembersView.qml's role pickers and RulesView.qml's add-rule row — rule 2(a), issue filed (#386). This is a real renderer gap, measured rather than assumed: converting them would have swapped a working 3-item ComboBox for a text box that accepts anything.

The README's old "Not exercised: morph::forms" section, which recorded the absence and deferred the decision to this issue, is replaced by the actual state.

The renderer gaps, all filed rather than worked around

Verification

Triage was explicit that schema generation working is necessary but not sufficient, and that nothing had been rendered on screen. So:

  • Offscreen GUI run: QT_QPA_PLATFORM=offscreen ladder_kanban_gui boots with zero output — no QML warning, no "produced no root object".
  • The forms are proven to render and submit by the new tests/test_gui_forms_render.cpp, which loads the shipped .qml into a real QQmlApplicationEngine with real bridges over a real backend, and per form asserts the controls drawn, the body assembled, and the row written. Not Qt Quick Test and it synthesizes no mouse events (TESTING.md rule 6) — it reproduces the two calls Submit makes.
  • Mutation-checked twice: removing the hidden-field seeding leaves CreateTask permanently !ready with previewLine == "{}"; removing hidden from CreateTask::columnId flips the raw-id column to visible == true. Both failed as expected, then were reverted.
  • ctest -L ladder-kanban100% tests passed out of 151 (baseline before this change: 140). Whole configure → 100% tests passed out of 1672.
  • test_morph_scenario.py and scenario_coverage.py exit 0, kanban still 22/22 actions and 20/20 workflows. check_spec_citations.sh, check_test_type_names.sh, check_rung_filters.sh all OK; clang-format clean.

Not verified: the test binary was not compiled under the strict warning set — -DMORPH_ENABLE_STRICT_COMPILATION=OFF, because #372 breaks every AUTOMOC target from a nested worktree. The two changed DTO headers were compiled standalone under the full -Weverything -Werror set: clean. No Remote-mode GUI session was run end to end — the offscreen run was Local mode, and the driven test uses BackendRig Mode::Local, so whether the desktop client emits #387's warning at runtime is inferred from the same code path, not measured. Whether other rungs (bookmarks' CreateBookmark::visibility) currently hit #386 was not checked. No docs/spec/ file was invalidated, so none changed.

🤖 Generated with Claude Code

https://claude.ai/code/session_01GnSnsebJ92Kti1D7m3RwYJ

…ast two

#357 converted the login form and left the rest of the showcase hand-built.
This finishes it: `CreateProject`, `CreateColumn`, `CreateSwimlane`,
`CreateTask` and `AddComment` now render from
`morph::forms::schemaJson<A>()` through the shipped `DynamicForm`, each with
the renderer's own explicit Submit button. Six forms on five screens, none
with a hand-written field or submit button.

`BoardBridge` grows the controller contract the renderer calls --
`schemasJson`, `submitIfValid`, `replyReceived` -- forwarding to a
`BoardPresenter::submitForm` that dispatches through `executeJson` and then
decodes the reply. Decoding is the load-bearing part: every board action
returns the full rebuilt state, so `submitForm` re-emits `boardOpened` (and
`commentAdded` for `AddComment`), which is what keeps `BoardBridge::board`
and every binding over it live in the same turn as the submit rather than at
the next poll tick. `ProjectAdminPresenter::submitForm` becomes the
two-model routing table its own comment predicted, and re-emits
`projectCreated` with the name recovered from the submitted body so the
project pane keeps re-listing itself. Both bridges serve one schema document
and route only their own actions; an action a controller does not serve is
reported on `replyReceived`, never dropped -- checked on `MoveTaskPosition`,
a registered action deliberately outside the form set, so the gate is proven
rather than the JSON decoder.

`createProject`, `createColumn`, `createSwimlane`, `createTask` and
`addComment` are demoted from `Q_INVOKABLE` to plain methods: nothing in
`gui/qml/` calls them any more, and dead QML surface is what
`test_kanban_qml_surface.cpp` exists to catch. `submitIfValid` needs the
opposite exemption on both bridges, its caller being the shipped
`DynamicForm.qml`.

Two DTO declarations carry the interaction the hand-built controls had.
`CreateTask`'s column/swimlane ids and `AddComment`'s task id are context,
not input -- the task goes in the column you are typing in, the comment on
the task whose popup is open -- so they are `hidden` in `fieldMetadata` and
seeded by the view that owns the form; no row id reaches the screen.
`CreateColumn::wipLimit` joins `optionalFields`, because 0 already means
unlimited and the `SpinBox` it replaced defaulted to it rather than
demanding a number.

Rendering these for real found what generating them could not, which is the
point triage made when it said schema generation is necessary and not
sufficient. `MembersView`'s role pickers and `RulesView`'s mutation-type
picker are *not* converted, and now carry rule 2(a)'s written justification
plus the finding rule 2 demands: `schemaJson` describes a `enum class`
completely, and `DynamicForm` draws it as a free-text field that reports
`ready` for `role = "Emperor"` (morph#386). Converting them would have
replaced two working combo boxes with two typo-accepting text boxes. Two
smaller renderer findings are filed rather than folded in: morph#387 (every
form warns about `onOptionsReceived` on a controller with no `Choice` field
-- invisible until a real controller is attached) and morph#388 (array-valued
schema keys mistyped when `schema` is assigned rather than bound). The
drag-and-drop board keeps its own rule 2(a) justification and files nothing:
its schema renders fine, and a form is simply the wrong shape for a gesture
whose inputs the gesture itself produces.

`tests/test_gui_forms_render.cpp` is what makes "rendered" more than a claim:
it loads the shipped `.qml` into a real engine with real bridges over a real
backend and, per form, asserts the controls drawn, the body assembled and the
row written. Mutation-checked twice -- dropping the hidden-field seeding
leaves `CreateTask` permanently `!ready`, and dropping `hidden` from
`CreateTask::columnId` makes the raw id column visible on screen.

151 kanban cases pass (was 140), 1672 in the whole configure; both scenario
scripts still exit 0. Built with `MORPH_ENABLE_STRICT_COMPILATION=OFF`
because morph#372 makes any AUTOMOC target unbuildable from a nested
worktree, so the test binary was not compiled under the strict warning set;
the two changed DTO headers were compiled separately under it and are clean.

Closes #344.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GnSnsebJ92Kti1D7m3RwYJ
@codecov

codecov Bot commented Sep 1, 2026

Copy link
Copy Markdown

@Yaraslaut
Yaraslaut merged commit 57f24ce into master Sep 1, 2026
39 checks passed
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.

The ladder's designated showcase GUI is hand-built end to end with zero morph::forms usage and no rule-2 justification

1 participant