Task: has_pagination knob is a silent-failure footgun in the Qt client model template

Table of Contents

This page documents a task in the Fix codegen template drift story. It captures the goal, current status, acceptance, and any notes or results.

1. Goal

cpp_qt_client_model.cpp.mustache generates a fetch method whose behaviour is a binary fork on the :has_pagination: Qt-facet flag: set true, it sends real offset=/=limit to the server and reports the server's true total_available_count; unset (the silent default), it ignores offset/limit entirely, always fetches the server's default page, and reports the fetched page's own size as if it were the total — masking the truncation instead of surfacing it. Every entity's MdiWindow unconditionally shows a full PaginationWidget (Next/Previous/Load All/page size) regardless of this flag, so an entity with the flag off looks identical to one with it on right up until its row count exceeds 100, at which point rows silently become unreachable with no error, warning, or visual cue. 61 of the app's ~89 Qt list entities currently have the flag unset — see the tracking task for the full list and the fix (set the flag, regenerate).

The narrower fix (setting the flag everywhere) doesn't address the shape of the footgun: a knob that silently defaults to the broken behaviour, with no build-time or run-time signal distinguishing "deliberately unpaginated, all rows always fit" from "someone forgot to set the flag." Worth reconsidering the underlying design once the narrow fix lands:

  • Generate the paginated fetch path unconditionally (drop the has_pagination fork) and let every entity always send real offset/limit and report the real total — the branch exists presumably as an optimisation/simplification for small, truly bounded lookup tables, but the two code paths are barely different and the unconditional one is strictly more correct.
  • Or, if a genuinely unpaginated mode is still wanted for small fixed lookup tables, gate the widget on the same flag too, so an unpaginated entity's MdiWindow doesn't render Next/Previous/Load All controls that don't do anything — the mismatch between "looks paginated" and "is paginated" is what let this go unnoticed across 61 entities.
  • Either way, a codegen lint/validation pass that flags entities whose generated model and generated window disagree on pagination support would catch the next occurrence of this drift at generation time rather than in production data.

2. Status

Field Value
State DONE
Parent story Fix codegen template drift
Now Nothing.
Waiting on Nothing.
Next Nothing.
Last touched 2026-09-04

3. Acceptance

  • ores.cpp.qt.client_model_impl.org (and the tanged cpp_qt_client_model.cpp.mustache) generates the fetch method with no has_pagination fork: the request always carries offset / limit and the model always takes total_available_count from the response. The {{^}} truncating branch and both {{#}} wrappers are gone, and the narrative no longer documents a two-branch design.
  • Re-tangling changes only the two files (org archetype + mustache).
  • The change is output-neutral for every entity that can regenerate today: regenerating the Qt client models (all flag-on) is byte-identical to the pre-edit output, so no model churn is attributable to the change beyond the recorded pre-existing drift baseline, and a second regeneration is byte-identical.
  • No entity can emit the truncating shape from this template any more: the 16 legacy-`* Primary key` entities cannot regenerate until their unblocking task lands (836BB4CE), and when they do, and for every new entity from now on, the generated fetch pages correctly with no per-entity knob to set. The unconditional PaginationWidget is therefore accurate for every generated model — the model/window disagreement class is gone.

4. Plan

  • 2026-09-04: survey. The flag's behavioural read surface is exactly one template: grep of the templates directory shows has_pagination consumed only by the client model fetch function (two {{#}} sites and one {{^}} branch, lines 311-364 of the org). Everywhere else it is inert documentation: the doc entity org templates echo :has_pagination: back into regenerated entity doc pages when set, and core.py (lines 3224-3225) loads the Qt drawer property with a silent False default to feed that same echo. doc_feature.org mentions the property only as an example literal in a generic feature template.
  • The protocol side has no gate: offset (default 0) / limit (default 100) and total_available_count are unconditional members of every generated list request and response class (ores.cpp.protocol.protocol_header.org, five request shapes, all with the same unconditional trio), so the paginated branch compiles for every entity when it can regenerate. The country-fix commit that created the fork (3f51aef9f4) changed only the model template, the country org, and the narrative — the flag never reached the protocol, the service, or the server.
  • Committed-model census: 30 entities carry the flag (the tracking task 914B206F's 29 fixes in PR #1760 plus calendar_type), all regenerable and byte-stable across the fixes' regeneration rounds. The only committed flag-off models belong to the 16 legacy-`* Primary key` heading entities (follow-up 836BB4CE) that cannot regenerate at all today — resolve_targets fails on any address for them, so their truncating output stays in the tree until that unblocking task lands, whatever this task decides.
  • Design decision: capture option 1 — generate the paginated fetch path unconditionally and delete the fork. The fork never was a small-table optimisation worth preserving: the {{^}} branch does not fetch "the whole set", it sends the request with its default offset=0/limit=100 and reports the returned page's size as the total, so both branches fetch at most the server's default page and differ only in whether the model can page onward and report a truthful total. The unconditional path is the one proven in production by the ~30 flag-on entities; for a table over 100 rows the flag-off path is strictly lossy. Deleting the fork makes the unconditional PaginationWidget correct for every generated model, which dissolves the model/window disagreement that let the bug go unnoticed across 61 entities — capture option 3 (a lint pass) is then unnecessary, and the flag itself survives only as the entity- org doc property it already is (loader and doc echo untouched; nothing behavioural reads it any more).
  • Implementation order: baseline full regeneration (drift record, pre-edit), org edit (unbrace the two {{#}} sites so the offset/limit pair and the response-total return are unconditional; delete the {{^}} branch; rewrite the narrative), re-tangle (only the one mustache should change), per-component regeneration (byte-identical to the baseline for all regenerable models), idempotency check (second regen byte-identical), then close out with the code+ci+docs verification union (site build, full build + ctest on a recreated database, drift checks, roundtrip).
  • 2026-09-04: regeneration evidence. The baseline full-ores regeneration (run 1, pre-edit) churned 202 tracked generated files — pre-existing drift whose composition (trading 95, sql 31, dq 15, scheduler 24 incl. Qt, synthetic 17 incl. Qt, database 7, analytics 4, compute 4, qt/iam 2, refdata/marketdata/iam 1 each) differs from task 453FA225's recorded baseline as main evolved; all of it reverted or excluded from this PR. The attributable delta between run 1 and run 2 (post-edit) is exactly 7 files: the two templates, two ores.database component_files.cmake (stub-byproduct churn, reverted), and three Qt client model flips (ClientBadgeSeverityModel, ClientReportInstanceModel — carried; ClientJobDefinitionModel — reverted, see Result). Runs 2 and 3 are byte-identical (identical md5 sets) — idempotent. The 166 untracked byproducts (42 inherited from task 453FA225's regens) are byte-identical across the runs.

5. Notes

6. Test Scenarios

Manual QA scenarios (scaffolded via compass add test_scenario, run through the QA Validation Runner panel) that verify this task. Link new ones here as they're created; the scenario doc itself links back via its "Verifies task" field.

Scenario State Notes
     

7. PRs

PR Title
#2006 [ores.codegen] Qt client model fetches paginated unconditionally

8. Review

# Comment summary File Decision Notes
1 Four reporting client models and ClientDatasetBundleModel still compiled on the truncating shape under ores.qt/compute/src and ores.qt/api/src ores.qt/compute/src/Client{ReportInstance,ReportType,ReportDefinition,ConcurrencyPolicy}Model.cpp, ores.qt/api/src/ClientDatasetBundleModel.cpp Follow-up Migration orphans: no generator writes to ores.qt/compute or ores.qt/api for these entities (canonical output goes to ores.qt/reporting and ores.qt/dq), so no regen can flip them; compiled via component_files.cmake and a duplicate-class (ODR) hazard while both libraries load — recorded as follow-up input for the migration/drift story, not foldable into this PR
2 ClientWorkspaceModel still on the truncating shape despite its protocol carrying offset/limit ores.qt/workspace/src/ClientWorkspaceModel.cpp Not changed Its org (ores.workspace.workspace.org) is legacy-`* Primary key`: codegen validation fails for it in both regen runs, so it cannot regenerate until the unblocking task 836BB4CE lands — the acceptance-recorded class, not a "trivial flip"
3 ClientJobDefinitionModel (scheduler) not flipped ores.qt/scheduler/src/ClientJobDefinitionModel.cpp Not changed Deferral recorded in Result and story Decisions: its regeneration bundles unrelated pre-existing drift (org evolved after its last regen), which never rides a foundation PR
4 variability_feature_has_pagination.org still documents the flag as a live behavioural toggle projects/modeling/variability_feature_has_pagination.org Fixed Feature retired; page now records the retirement (task 9FF6FAD3) and the flag's documentation-only survival

9. Result

Delivered 2026-09-04 on branch feature/pagination-knob-fragility, commit 7287a46527.

  • The fetch is unconditional in both the org source (ores.cpp.qt.client_model_impl.org) and the tangled mustache: the {{#domain_entity.qt.has_pagination}} wrappers around the offset=/=limit pair and around the log + response-total return are gone, and the {{^}} truncating branch (defaults, page-size reported as total) is deleted. The request always carries offset=/=limit and the model always takes total_available_count from the response, so no entity can emit the truncating shape from this template. The section (now "** Fetching a page") records the fork's history; the flag survives only as entity-org documentation (loader and doc echo untouched).
  • Re-tangling changed only the two template files (the org and its tangled mustache).
  • Regeneration evidence (three full-ores regens, md5 per changed file, in the Plan): run 1 (pre-edit) churned 202 tracked generated files of pre-existing drift — all reverted or excluded from this PR and input for the per-component drift stories. The attributable delta between run 1 and run 2 is exactly 7 files: the two templates, two component_files.cmake files (stub-byproduct churn, reverted), and three Qt client model flips. Runs 2 and 3 are byte-identical (identical md5 sets) — idempotent.
  • Census correction: the survey census (30 flag-on entities, 16 legacy-`* Primary key` unregenerable) missed three regenerable modern-key entities that carry the flag off: the regen delta flipped ClientBadgeSeverityModel (dq) and ClientReportInstanceModel (reporting), both carried in this PR as the intended repair — the tracking task 914B206F's census never listed them. The narrow fix (set the flag everywhere) was therefore incomplete exactly where this fix cannot be: there is no flag left to forget. The third flip, ClientJobDefinitionModel (scheduler), sits on top of stale committed drift (its org evolved after its last regeneration), so the file was reverted to its committed state and its regeneration is left to the scheduler per-component drift story; until then its committed model stays the old truncating shape, a historical artifact this template can no longer produce.
  • Output neutrality held for every flag-on entity: no Qt client model churn exists in any flag-on component across the runs beyond the three flips above, and the post-fix component drift check (refdata) wrote every flag-on Qt model byte-identical to the committed tree.
  • Verification (change class: code + ci + docs): site build clean; full system build clean (linux-clang-debug-make); ctest 71/71 passed against a recreated database — the CI-equivalent state, because the repository write tests are not repeatable against a persistent database; component drift checks (refdata, reporting, marketdata) reproduce the committed tree except the three recorded pre-existing files (party_repository.cpp, feed_binding.hpp and its create sql), reverted after the check; ore domain roundtrip report generated clean (rc 0), structurally unchanged — this diff touches no roundtrip input or parser.
  • The 166 untracked regen byproducts (42 inherited from task 453FA225's regen plus this unit's additions) stay in the working tree, byte-identical across runs 2 and 3, excluded from all commits (deletion not authorised).

Acceptance: bullets 1, 2, and 4 met as written. Bullet 3 holds for its flag-on scope with the correction recorded above: the two census-missed flag-off entities flip as the intended repair (which is the story's purpose — an entity with the flag off must not truncate), and no other model churn is attributable to the change.

10. Promoted from capture

Captured 2026-07-19 in the product backlog; promoted preserving the UUID.

10.1. What

cpp_qt_client_model.cpp.mustache generates a fetch method whose behaviour is a binary fork on the :has_pagination: Qt-facet flag: set true, it sends real offset=/=limit to the server and reports the server's true total_available_count; unset (the silent default), it ignores offset/limit entirely, always fetches the server's default page, and reports the fetched page's own size as if it were the total — masking the truncation instead of surfacing it. Every entity's MdiWindow unconditionally shows a full PaginationWidget (Next/Previous/Load All/page size) regardless of this flag, so an entity with the flag off looks identical to one with it on right up until its row count exceeds 100, at which point rows silently become unreachable with no error, warning, or visual cue. 61 of the app's ~89 Qt list entities currently have the flag unset — see the tracking task for the full list and the fix (set the flag, regenerate).

The narrower fix (setting the flag everywhere) doesn't address the shape of the footgun: a knob that silently defaults to the broken behaviour, with no build-time or run-time signal distinguishing "deliberately unpaginated, all rows always fit" from "someone forgot to set the flag." Worth reconsidering the underlying design once the narrow fix lands:

  • Generate the paginated fetch path unconditionally (drop the has_pagination fork) and let every entity always send real offset/limit and report the real total — the branch exists presumably as an optimisation/simplification for small, truly bounded lookup tables, but the two code paths are barely different and the unconditional one is strictly more correct.
  • Or, if a genuinely unpaginated mode is still wanted for small fixed lookup tables, gate the widget on the same flag too, so an unpaginated entity's MdiWindow doesn't render Next/Previous/Load All controls that don't do anything — the mismatch between "looks paginated" and "is paginated" is what let this go unnoticed across 61 entities.
  • Either way, a codegen lint/validation pass that flags entities whose generated model and generated window disagree on pagination support would catch the next occurrence of this drift at generation time rather than in production data.

10.2. Why

Discovered while investigating a real bug during manual QA of the calendar Qt screens task: the Currencies list showed "Showing all records (100)" with 68 currencies (including USD) silently unreachable. The immediate fix is per-entity (tracked here); this capture is about the generator-level anti-pattern that made the bug possible and easy to reintroduce on the next new entity.

10.3. References

Emacs 29.3 (Org mode 9.6.15)