From c78b44a4d0eabf63ab24e7a1a9ed4d4cffe1ab6d Mon Sep 17 00:00:00 2001 From: Yaraslau Tamashevich Date: Thu, 24 Sep 2026 02:50:12 +0200 Subject: [PATCH 01/10] comments(examples): state the tidy suppressions' constraints, drop the citations Every `.clang-tidy` under `examples/` -- the shared testkit one, seven rung `tests/` copies and eight rung `include/` copies. The suppressions' arguments, probe tables and measured counts all stay; what goes is the ticket numbers, the narration of what a paragraph used to claim, and the description of gates that no longer exist. Where a removed gate was the only thing holding a placement rule, the rule is now stated as one to keep by hand, which is what is true. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW --- examples/bank/include/.clang-tidy | 40 +++++++++------------ examples/bank/tests/.clang-tidy | 13 ++++--- examples/bookmarks/include/.clang-tidy | 21 ++++++----- examples/bookmarks/tests/.clang-tidy | 13 ++++--- examples/common/testkit/.clang-tidy | 49 +++++++++++--------------- examples/crm/include/.clang-tidy | 21 ++++++----- examples/crm/tests/.clang-tidy | 13 ++++--- examples/kanban/include/.clang-tidy | 21 ++++++----- examples/kanban/tests/.clang-tidy | 13 ++++--- examples/ledger/include/.clang-tidy | 21 ++++++----- examples/ledger/tests/.clang-tidy | 13 ++++--- examples/lims/include/.clang-tidy | 21 ++++++----- examples/lims/tests/.clang-tidy | 13 ++++--- examples/pastebin/include/.clang-tidy | 21 ++++++----- examples/pastebin/tests/.clang-tidy | 13 ++++--- examples/polls/include/.clang-tidy | 21 ++++++----- examples/polls/tests/.clang-tidy | 13 ++++--- 17 files changed, 154 insertions(+), 186 deletions(-) diff --git a/examples/bank/include/.clang-tidy b/examples/bank/include/.clang-tidy index e534c2745..c15db5408 100644 --- a/examples/bank/include/.clang-tidy +++ b/examples/bank/include/.clang-tidy @@ -87,20 +87,19 @@ # no `TableName` to exempt; the ignore has nothing to do there and does # nothing. # -# One directory up, at `examples/`, is not available and should not be: -# a gate (since removed) failed outright if `examples/.clang-tidy` -# existed, because a config there governs every rung's src/, include/ and -# gui_lib/ together and is the shape in which a narrow suppression quietly -# becomes a wide one. That gate was written about the Catch2 suppression, and -# the reasoning holds here regardless of how narrow this key is: the file would -# be reaching seven rungs' production code on the strength of an argument made -# about the bank example. +# One directory up, at `examples/`, is not available and should not be: a +# config there governs every rung's src/, include/ and gui_lib/ together, which +# is the shape in which a narrow suppression quietly becomes a wide one. That +# holds regardless of how narrow this key is -- the file would be reaching seven +# rungs' production code on the strength of an argument made about the bank +# example. Nothing gates that placement any more, so it is a rule to keep by +# hand. # # That leaves the other 59 declarations -- 13 in crm's `db/`, 12 in ledger's, # 11 in kanban's, 9 in lims', 6 in polls', 5 in bookmarks', 1 in pastebin's, # and 2 in examples/common/testkit/ -- unexempted and, today, unreported: # clang-tidy-diff analyses only changed lines, so a declaration nobody has -# touched has never been judged (morph#677). They are latent, not fixed, and +# touched is never judged. They are latent, not fixed, and # deliberately left to the rung that owns each one rather than settled from # here. The two under examples/common/testkit/ are the sharpest of them, # because they sit in .cpp files that do have compile commands in this job's @@ -111,10 +110,9 @@ # clang-tidy *replaces* the root configuration rather than extending it, and # this file's `CheckOptions` alone enable nothing -- the whole check list gone. # -# This paragraph used to say that happened *silently*, "while still exiting 0 -# on a clean file". It does not (morph#716). Re-measured on this revision with -# clang-tidy 22.1.8 and the same compile database as the probe above, which is -# the gap the earlier measurement left open: +# Dropping the key fails loudly rather than silently, which is worth knowing +# before reasoning about whether some other config needs protecting. Measured +# with clang-tidy 22.1.8 and the same compile database as the probe above: # # $ sed -i 's/^InheritParentConfig: true$/# &/' examples/bank/include/.clang-tidy # $ clang-tidy -p build examples/bank/include/bank/offline/lightweight_offline_queue.hpp @@ -123,14 +121,9 @@ # 1 # # against `6 warnings treated as errors` from the same command with the key in -# place. So dropping the key is a red build with a message naming the problem, -# not a quietly unlinted directory. The conclusion is unchanged -- the key is -# required, and the gate that checked for it in every -# examples//tests/.clang-tidy was right to -- it has since been removed, -# and the reason is a loud failure caught -# early rather than a silent one caught never, and a reader deciding whether -# some other config needs the same protection should be reasoning from the -# former. +# place. So the key is required and its absence is a red build with a message +# naming the problem, not a quietly unlinted directory. Nothing checks for the +# key, so keeping it here is a rule to hold by hand. # # ── Reach, stated rather than guessed ──────────────────────────────────────── # @@ -139,9 +132,8 @@ # story: the root `HeaderFilterRegex` is `include/morph/.*`, which no example # header matches, so a `TableName` reached from a .cpp is discarded as non-user # code regardless of what this file says. The case that reaches CI is -# clang-tidy-diff analysing a *changed header* as a main file, which is how -# bank/offline/lightweight_offline_queue.hpp became the first of the 71 -# declarations ever to be judged at all. +# clang-tidy-diff analysing a *changed header* as a main file, which is the +# only way any of these 71 declarations is judged at all. # # The residue: a local variable or ordinary member spelled exactly `TableName`, # written anywhere under examples/bank/include/ for some reason unrelated to diff --git a/examples/bank/tests/.clang-tidy b/examples/bank/tests/.clang-tidy index b1496f1c0..3a3c851de 100644 --- a/examples/bank/tests/.clang-tidy +++ b/examples/bank/tests/.clang-tidy @@ -16,7 +16,7 @@ # a workstation with a current Catch2. That number is checked rather than # asserted: scripts/check_catch2_pin.sh reads the runner's installed version # back off the include path and fails this line and the clang-tidy-diff job -# together if the package moves (morph#666). +# together if the package moves. # # Directory-scoped, and no wider. clang-tidy resolves configuration by walking # up from the file it is analysing and offers no finer granularity than a @@ -28,9 +28,8 @@ # entry is subtracted, and only here. # # One copy per test directory rather than one shared file, because no directory -# contains every example's tests and nothing else. A gate used to fail if a test -# directory on the application ladder had no such file; it was removed with the -# meta-gates, so a new rung now needs this file added by hand. +# contains every example's tests and nothing else. Nothing gates the file's +# presence, so a new rung needs this file added by hand. # readability-function-cognitive-complexity: the check stays ON here. What is # turned off is its counting of increments that come from *inside* a macro @@ -38,10 +37,10 @@ # assertion macro expands to `do { try { ... } catch (...) { ... } } while (...)` # and the check scores that at 4 whatever it is asserting. Seven assertions is # 28 against a threshold of 25, so a TEST_CASE goes over the line for having -# assertions in it (morph#778). +# assertions in it. # -# `IgnoreMacros: true` subtracts exactly that and nothing else. Measured on -# 68a30bcc with clang-tidy 22.1.8 over the 751 translation units the +# `IgnoreMacros: true` subtracts exactly that and nothing else. Measured with +# clang-tidy 22.1.8 over the 751 translation units the # clang-tidy-diff job's configure produces, the check forced on everywhere and # the only variable this option: # diff --git a/examples/bookmarks/include/.clang-tidy b/examples/bookmarks/include/.clang-tidy index a79cfe77b..a0bf685de 100644 --- a/examples/bookmarks/include/.clang-tidy +++ b/examples/bookmarks/include/.clang-tidy @@ -4,9 +4,9 @@ # not a check, not a declaration kind, not a directory's worth of code. # # The long form of this argument, and the probe runs behind every claim below, -# are in examples/bank/include/.clang-tidy, landed by morph#697. This file is -# the same key applied to one more header tree (morph#702), with this rung's -# own numbers rather than bank's. The short form: +# are in examples/bank/include/.clang-tidy. This file is the same key applied to +# one more header tree, with this rung's own numbers rather than bank's. The +# short form: # # * The repository-root .clang-tidy sets # `readability-identifier-naming.VariableCase: camelBack`, and a Lightweight @@ -51,18 +51,17 @@ # clang-tidy *replaces* the root configuration rather than extending it. # Measured on the same probe with the key removed and nothing else changed: # `Error: no checks enabled.`, exit 1, both from a direct clang-tidy run -# and through clang-tidy-diff.py -- which is louder than -# examples/bank/include/.clang-tidy records for the same mutation -# (morph#716). +# and through clang-tidy-diff.py. So its absence is a red build with a +# message naming the problem, not a quietly unlinted directory. # # ── Scope, and why it is this directory ────────────────────────────────────── # # clang-tidy resolves configuration by walking up from the file it is # analysing and offers no finer granularity than a directory, so the only -# choice is which directory. One up, at `examples/`, was refused outright by a -# gate (since removed), because a config there governs every rung's -# src/, include/ and gui_lib/ together -- a narrow suppression in the shape -# that quietly becomes a wide one. +# choice is which directory. One up, at `examples/`, is refused: a config there +# governs every rung's src/, include/ and gui_lib/ together -- a narrow +# suppression in the shape that quietly becomes a wide one. Nothing gates that +# placement, so it is a rule to keep by hand. # # `examples/bookmarks/include/` is this rung's own header tree. It holds 23 # .hpp and no .cpp at all, so this file governs declarations and nothing @@ -70,7 +69,7 @@ # (`include/morph/.*`) -- so a `TableName` reached from some .cpp is # discarded as non-user code whatever this file says. The case that reaches # CI is clang-tidy-diff analysing a *changed header* as a main file, which is -# the only way any of these declarations has ever been judged (morph#677). +# the only way any of these declarations is judged at all. # # Reach on this revision: 5 of the repository's 71 `static constexpr # std::string_view TableName` declarations are under this directory, one each diff --git a/examples/bookmarks/tests/.clang-tidy b/examples/bookmarks/tests/.clang-tidy index b1496f1c0..3a3c851de 100644 --- a/examples/bookmarks/tests/.clang-tidy +++ b/examples/bookmarks/tests/.clang-tidy @@ -16,7 +16,7 @@ # a workstation with a current Catch2. That number is checked rather than # asserted: scripts/check_catch2_pin.sh reads the runner's installed version # back off the include path and fails this line and the clang-tidy-diff job -# together if the package moves (morph#666). +# together if the package moves. # # Directory-scoped, and no wider. clang-tidy resolves configuration by walking # up from the file it is analysing and offers no finer granularity than a @@ -28,9 +28,8 @@ # entry is subtracted, and only here. # # One copy per test directory rather than one shared file, because no directory -# contains every example's tests and nothing else. A gate used to fail if a test -# directory on the application ladder had no such file; it was removed with the -# meta-gates, so a new rung now needs this file added by hand. +# contains every example's tests and nothing else. Nothing gates the file's +# presence, so a new rung needs this file added by hand. # readability-function-cognitive-complexity: the check stays ON here. What is # turned off is its counting of increments that come from *inside* a macro @@ -38,10 +37,10 @@ # assertion macro expands to `do { try { ... } catch (...) { ... } } while (...)` # and the check scores that at 4 whatever it is asserting. Seven assertions is # 28 against a threshold of 25, so a TEST_CASE goes over the line for having -# assertions in it (morph#778). +# assertions in it. # -# `IgnoreMacros: true` subtracts exactly that and nothing else. Measured on -# 68a30bcc with clang-tidy 22.1.8 over the 751 translation units the +# `IgnoreMacros: true` subtracts exactly that and nothing else. Measured with +# clang-tidy 22.1.8 over the 751 translation units the # clang-tidy-diff job's configure produces, the check forced on everywhere and # the only variable this option: # diff --git a/examples/common/testkit/.clang-tidy b/examples/common/testkit/.clang-tidy index 7591680bc..9c80276ae 100644 --- a/examples/common/testkit/.clang-tidy +++ b/examples/common/testkit/.clang-tidy @@ -16,7 +16,7 @@ # a workstation with a current Catch2. That number is checked rather than # asserted: scripts/check_catch2_pin.sh reads the runner's installed version # back off the include path and fails this line and the clang-tidy-diff job -# together if the package moves (morph#666). +# together if the package moves. # # Directory-scoped, and no wider. clang-tidy resolves configuration by walking # up from the file it is analysing and offers no finer granularity than a @@ -27,46 +27,37 @@ # keeps every other check the repository-root .clang-tidy enables: exactly one # entry is subtracted, and only here. # -# ── What "only here" is worth, and how it is kept true (morph#652) ─────────── +# ── What "only here" is worth, and what keeps it true ──────────────────────── # -# That sentence was false for two years' worth of commits, in the way this -# repository finds hardest to notice: the directory held the testkit *library* -# as well as the testkit's tests. `fault_proxy.cpp` and `qml_surface.cpp` -- -# the two translation units of `morph_ladder_testkit`, neither containing a -# single REQUIRE -- resolved their configuration from this file, so the check -# was off for them on the strength of an argument about Catch2 that did not -# apply to them at all. "The code under test" was in the directory with the -# test, which is exactly the case the paragraph above says cannot happen. -# -# They now live in `examples/common/testkit_src/`, one directory up and -# outside this file's reach, so the claim holds by construction. It is not -# left to hold by assertion either -- a gate (since removed) failed if any -# .cpp in a directory carrying this suppression was not a Catch2 translation -# unit -- no TEST_CASE, no SCENARIO, no catch2/ include -- which is the shape -# a second library source arriving here would have. That check is the reason -# the claim above can be read as a fact rather than an intention. +# "Only here" is worth nothing if a library source can sit in this directory: +# it would resolve its configuration from this file and get the check switched +# off on the strength of an argument about Catch2 that does not apply to it. +# The two translation units of `morph_ladder_testkit` -- `fault_proxy.cpp` and +# `qml_surface.cpp`, neither containing a single REQUIRE -- are therefore kept +# in `examples/common/testkit_src/`, one directory up and outside this file's +# reach. Nothing enforces that placement automatically, so adding a library +# .cpp here is the way to break the paragraph above; add it to testkit_src/ +# instead. # # What is *not* narrowed, and cannot be: the reusable headers beside the tests # (backend_rig.hpp, pump.hpp, db_fixture.hpp, ...) are library code too, and a # header reached from a TU under this directory is analysed with that TU's -# configuration, not its own (morph#632 measured the same effect for +# configuration, not its own. Measured for the equivalent case in # tests/.clang-tidy: 333 findings inside include/morph/** hidden across 25 -# headers). So a chained comparison written by hand in one of those headers is +# headers. So a chained comparison written by hand in one of those headers is # unreported whenever the reaching TU is one of this directory's tests -- and # since every other consumer is a rung's test binary, reaching them from an # examples//tests/ that carries its own copy of this same one entry, in # practice it is unreported from everywhere. That is the residue, stated -# rather than narrowed, for morph#632's reason: a suppression whose reach is -# written down is auditable, and one whose reach is guessed is not. It is -# bounded by what the check is -- `clang-diagnostic-parentheses`, which this -# file does not subtract, reports the same `a < b < c` shape independently -# and was observed doing so on the probe morph#652 was measured with. +# rather than narrowed: a suppression whose reach is written down is auditable, +# and one whose reach is guessed is not. It is bounded by what the check is -- +# `clang-diagnostic-parentheses`, which this file does not subtract, reports +# the same `a < b < c` shape independently, and was observed doing so on the +# probe this residue was measured with. # # One copy per test directory rather than one shared file, because no directory -# contains every example's tests and nothing else. A gate used to fail if a test -# directory on the application ladder had no such file; it was removed with the -# meta-gates, so a new rung now needs this file added by hand. - +# contains every example's tests and nothing else. Nothing gates the file's +# presence, so a new rung needs this file added by hand. Checks: '-bugprone-chained-comparison' InheritParentConfig: true diff --git a/examples/crm/include/.clang-tidy b/examples/crm/include/.clang-tidy index 66209d0a1..e448a044f 100644 --- a/examples/crm/include/.clang-tidy +++ b/examples/crm/include/.clang-tidy @@ -4,9 +4,9 @@ # not a check, not a declaration kind, not a directory's worth of code. # # The long form of this argument, and the probe runs behind every claim below, -# are in examples/bank/include/.clang-tidy, landed by morph#697. This file is -# the same key applied to one more header tree (morph#702), with this rung's -# own numbers rather than bank's. The short form: +# are in examples/bank/include/.clang-tidy. This file is the same key applied to +# one more header tree, with this rung's own numbers rather than bank's. The +# short form: # # * The repository-root .clang-tidy sets # `readability-identifier-naming.VariableCase: camelBack`, and a Lightweight @@ -51,18 +51,17 @@ # clang-tidy *replaces* the root configuration rather than extending it. # Measured on the same probe with the key removed and nothing else changed: # `Error: no checks enabled.`, exit 1, both from a direct clang-tidy run -# and through clang-tidy-diff.py -- which is louder than -# examples/bank/include/.clang-tidy records for the same mutation -# (morph#716). +# and through clang-tidy-diff.py. So its absence is a red build with a +# message naming the problem, not a quietly unlinted directory. # # ── Scope, and why it is this directory ────────────────────────────────────── # # clang-tidy resolves configuration by walking up from the file it is # analysing and offers no finer granularity than a directory, so the only -# choice is which directory. One up, at `examples/`, was refused outright by a -# gate (since removed), because a config there governs every rung's -# src/, include/ and gui_lib/ together -- a narrow suppression in the shape -# that quietly becomes a wide one. +# choice is which directory. One up, at `examples/`, is refused: a config there +# governs every rung's src/, include/ and gui_lib/ together -- a narrow +# suppression in the shape that quietly becomes a wide one. Nothing gates that +# placement, so it is a rule to keep by hand. # # `examples/crm/include/` is this rung's own header tree. It holds 25 .hpp # and no .cpp at all, so this file governs declarations and nothing else, and @@ -70,7 +69,7 @@ # (`include/morph/.*`) -- so a `TableName` reached from some .cpp is # discarded as non-user code whatever this file says. The case that reaches # CI is clang-tidy-diff analysing a *changed header* as a main file, which is -# the only way any of these declarations has ever been judged (morph#677). +# the only way any of these declarations is judged at all. # # Reach on this revision: 13 of the repository's 71 `static constexpr # std::string_view TableName` declarations are under this directory, all diff --git a/examples/crm/tests/.clang-tidy b/examples/crm/tests/.clang-tidy index b1496f1c0..3a3c851de 100644 --- a/examples/crm/tests/.clang-tidy +++ b/examples/crm/tests/.clang-tidy @@ -16,7 +16,7 @@ # a workstation with a current Catch2. That number is checked rather than # asserted: scripts/check_catch2_pin.sh reads the runner's installed version # back off the include path and fails this line and the clang-tidy-diff job -# together if the package moves (morph#666). +# together if the package moves. # # Directory-scoped, and no wider. clang-tidy resolves configuration by walking # up from the file it is analysing and offers no finer granularity than a @@ -28,9 +28,8 @@ # entry is subtracted, and only here. # # One copy per test directory rather than one shared file, because no directory -# contains every example's tests and nothing else. A gate used to fail if a test -# directory on the application ladder had no such file; it was removed with the -# meta-gates, so a new rung now needs this file added by hand. +# contains every example's tests and nothing else. Nothing gates the file's +# presence, so a new rung needs this file added by hand. # readability-function-cognitive-complexity: the check stays ON here. What is # turned off is its counting of increments that come from *inside* a macro @@ -38,10 +37,10 @@ # assertion macro expands to `do { try { ... } catch (...) { ... } } while (...)` # and the check scores that at 4 whatever it is asserting. Seven assertions is # 28 against a threshold of 25, so a TEST_CASE goes over the line for having -# assertions in it (morph#778). +# assertions in it. # -# `IgnoreMacros: true` subtracts exactly that and nothing else. Measured on -# 68a30bcc with clang-tidy 22.1.8 over the 751 translation units the +# `IgnoreMacros: true` subtracts exactly that and nothing else. Measured with +# clang-tidy 22.1.8 over the 751 translation units the # clang-tidy-diff job's configure produces, the check forced on everywhere and # the only variable this option: # diff --git a/examples/kanban/include/.clang-tidy b/examples/kanban/include/.clang-tidy index 772c2399c..dcb4594ee 100644 --- a/examples/kanban/include/.clang-tidy +++ b/examples/kanban/include/.clang-tidy @@ -4,9 +4,9 @@ # not a check, not a declaration kind, not a directory's worth of code. # # The long form of this argument, and the probe runs behind every claim below, -# are in examples/bank/include/.clang-tidy, landed by morph#697. This file is -# the same key applied to one more header tree (morph#702), with this rung's -# own numbers rather than bank's. The short form: +# are in examples/bank/include/.clang-tidy. This file is the same key applied to +# one more header tree, with this rung's own numbers rather than bank's. The +# short form: # # * The repository-root .clang-tidy sets # `readability-identifier-naming.VariableCase: camelBack`, and a Lightweight @@ -51,18 +51,17 @@ # clang-tidy *replaces* the root configuration rather than extending it. # Measured on the same probe with the key removed and nothing else changed: # `Error: no checks enabled.`, exit 1, both from a direct clang-tidy run -# and through clang-tidy-diff.py -- which is louder than -# examples/bank/include/.clang-tidy records for the same mutation -# (morph#716). +# and through clang-tidy-diff.py. So its absence is a red build with a +# message naming the problem, not a quietly unlinted directory. # # ── Scope, and why it is this directory ────────────────────────────────────── # # clang-tidy resolves configuration by walking up from the file it is # analysing and offers no finer granularity than a directory, so the only -# choice is which directory. One up, at `examples/`, was refused outright by a -# gate (since removed), because a config there governs every rung's -# src/, include/ and gui_lib/ together -- a narrow suppression in the shape -# that quietly becomes a wide one. +# choice is which directory. One up, at `examples/`, is refused: a config there +# governs every rung's src/, include/ and gui_lib/ together -- a narrow +# suppression in the shape that quietly becomes a wide one. Nothing gates that +# placement, so it is a rule to keep by hand. # # `examples/kanban/include/` is this rung's own header tree. It holds 17 .hpp # and no .cpp at all, so this file governs declarations and nothing else, and @@ -70,7 +69,7 @@ # (`include/morph/.*`) -- so a `TableName` reached from some .cpp is # discarded as non-user code whatever this file says. The case that reaches # CI is clang-tidy-diff analysing a *changed header* as a main file, which is -# the only way any of these declarations has ever been judged (morph#677). +# the only way any of these declarations is judged at all. # # Reach on this revision: 11 of the repository's 71 `static constexpr # std::string_view TableName` declarations are under this directory, all diff --git a/examples/kanban/tests/.clang-tidy b/examples/kanban/tests/.clang-tidy index b1496f1c0..3a3c851de 100644 --- a/examples/kanban/tests/.clang-tidy +++ b/examples/kanban/tests/.clang-tidy @@ -16,7 +16,7 @@ # a workstation with a current Catch2. That number is checked rather than # asserted: scripts/check_catch2_pin.sh reads the runner's installed version # back off the include path and fails this line and the clang-tidy-diff job -# together if the package moves (morph#666). +# together if the package moves. # # Directory-scoped, and no wider. clang-tidy resolves configuration by walking # up from the file it is analysing and offers no finer granularity than a @@ -28,9 +28,8 @@ # entry is subtracted, and only here. # # One copy per test directory rather than one shared file, because no directory -# contains every example's tests and nothing else. A gate used to fail if a test -# directory on the application ladder had no such file; it was removed with the -# meta-gates, so a new rung now needs this file added by hand. +# contains every example's tests and nothing else. Nothing gates the file's +# presence, so a new rung needs this file added by hand. # readability-function-cognitive-complexity: the check stays ON here. What is # turned off is its counting of increments that come from *inside* a macro @@ -38,10 +37,10 @@ # assertion macro expands to `do { try { ... } catch (...) { ... } } while (...)` # and the check scores that at 4 whatever it is asserting. Seven assertions is # 28 against a threshold of 25, so a TEST_CASE goes over the line for having -# assertions in it (morph#778). +# assertions in it. # -# `IgnoreMacros: true` subtracts exactly that and nothing else. Measured on -# 68a30bcc with clang-tidy 22.1.8 over the 751 translation units the +# `IgnoreMacros: true` subtracts exactly that and nothing else. Measured with +# clang-tidy 22.1.8 over the 751 translation units the # clang-tidy-diff job's configure produces, the check forced on everywhere and # the only variable this option: # diff --git a/examples/ledger/include/.clang-tidy b/examples/ledger/include/.clang-tidy index e13f7cad0..a735c1d0d 100644 --- a/examples/ledger/include/.clang-tidy +++ b/examples/ledger/include/.clang-tidy @@ -4,9 +4,9 @@ # not a check, not a declaration kind, not a directory's worth of code. # # The long form of this argument, and the probe runs behind every claim below, -# are in examples/bank/include/.clang-tidy, landed by morph#697. This file is -# the same key applied to one more header tree (morph#702), with this rung's -# own numbers rather than bank's. The short form: +# are in examples/bank/include/.clang-tidy. This file is the same key applied to +# one more header tree, with this rung's own numbers rather than bank's. The +# short form: # # * The repository-root .clang-tidy sets # `readability-identifier-naming.VariableCase: camelBack`, and a Lightweight @@ -51,18 +51,17 @@ # clang-tidy *replaces* the root configuration rather than extending it. # Measured on the same probe with the key removed and nothing else changed: # `Error: no checks enabled.`, exit 1, both from a direct clang-tidy run -# and through clang-tidy-diff.py -- which is louder than -# examples/bank/include/.clang-tidy records for the same mutation -# (morph#716). +# and through clang-tidy-diff.py. So its absence is a red build with a +# message naming the problem, not a quietly unlinted directory. # # ── Scope, and why it is this directory ────────────────────────────────────── # # clang-tidy resolves configuration by walking up from the file it is # analysing and offers no finer granularity than a directory, so the only -# choice is which directory. One up, at `examples/`, was refused outright by a -# gate (since removed), because a config there governs every rung's -# src/, include/ and gui_lib/ together -- a narrow suppression in the shape -# that quietly becomes a wide one. +# choice is which directory. One up, at `examples/`, is refused: a config there +# governs every rung's src/, include/ and gui_lib/ together -- a narrow +# suppression in the shape that quietly becomes a wide one. Nothing gates that +# placement, so it is a rule to keep by hand. # # `examples/ledger/include/` is this rung's own header tree. It holds 22 .hpp # and no .cpp at all, so this file governs declarations and nothing else, and @@ -70,7 +69,7 @@ # (`include/morph/.*`) -- so a `TableName` reached from some .cpp is # discarded as non-user code whatever this file says. The case that reaches # CI is clang-tidy-diff analysing a *changed header* as a main file, which is -# the only way any of these declarations has ever been judged (morph#677). +# the only way any of these declarations is judged at all. # # Reach on this revision: 12 of the repository's 71 `static constexpr # std::string_view TableName` declarations are under this directory, all diff --git a/examples/ledger/tests/.clang-tidy b/examples/ledger/tests/.clang-tidy index b1496f1c0..3a3c851de 100644 --- a/examples/ledger/tests/.clang-tidy +++ b/examples/ledger/tests/.clang-tidy @@ -16,7 +16,7 @@ # a workstation with a current Catch2. That number is checked rather than # asserted: scripts/check_catch2_pin.sh reads the runner's installed version # back off the include path and fails this line and the clang-tidy-diff job -# together if the package moves (morph#666). +# together if the package moves. # # Directory-scoped, and no wider. clang-tidy resolves configuration by walking # up from the file it is analysing and offers no finer granularity than a @@ -28,9 +28,8 @@ # entry is subtracted, and only here. # # One copy per test directory rather than one shared file, because no directory -# contains every example's tests and nothing else. A gate used to fail if a test -# directory on the application ladder had no such file; it was removed with the -# meta-gates, so a new rung now needs this file added by hand. +# contains every example's tests and nothing else. Nothing gates the file's +# presence, so a new rung needs this file added by hand. # readability-function-cognitive-complexity: the check stays ON here. What is # turned off is its counting of increments that come from *inside* a macro @@ -38,10 +37,10 @@ # assertion macro expands to `do { try { ... } catch (...) { ... } } while (...)` # and the check scores that at 4 whatever it is asserting. Seven assertions is # 28 against a threshold of 25, so a TEST_CASE goes over the line for having -# assertions in it (morph#778). +# assertions in it. # -# `IgnoreMacros: true` subtracts exactly that and nothing else. Measured on -# 68a30bcc with clang-tidy 22.1.8 over the 751 translation units the +# `IgnoreMacros: true` subtracts exactly that and nothing else. Measured with +# clang-tidy 22.1.8 over the 751 translation units the # clang-tidy-diff job's configure produces, the check forced on everywhere and # the only variable this option: # diff --git a/examples/lims/include/.clang-tidy b/examples/lims/include/.clang-tidy index 7d63798ea..f59417bb5 100644 --- a/examples/lims/include/.clang-tidy +++ b/examples/lims/include/.clang-tidy @@ -4,9 +4,9 @@ # not a check, not a declaration kind, not a directory's worth of code. # # The long form of this argument, and the probe runs behind every claim below, -# are in examples/bank/include/.clang-tidy, landed by morph#697. This file is -# the same key applied to one more header tree (morph#702), with this rung's -# own numbers rather than bank's. The short form: +# are in examples/bank/include/.clang-tidy. This file is the same key applied to +# one more header tree, with this rung's own numbers rather than bank's. The +# short form: # # * The repository-root .clang-tidy sets # `readability-identifier-naming.VariableCase: camelBack`, and a Lightweight @@ -51,18 +51,17 @@ # clang-tidy *replaces* the root configuration rather than extending it. # Measured on the same probe with the key removed and nothing else changed: # `Error: no checks enabled.`, exit 1, both from a direct clang-tidy run -# and through clang-tidy-diff.py -- which is louder than -# examples/bank/include/.clang-tidy records for the same mutation -# (morph#716). +# and through clang-tidy-diff.py. So its absence is a red build with a +# message naming the problem, not a quietly unlinted directory. # # ── Scope, and why it is this directory ────────────────────────────────────── # # clang-tidy resolves configuration by walking up from the file it is # analysing and offers no finer granularity than a directory, so the only -# choice is which directory. One up, at `examples/`, was refused outright by a -# gate (since removed), because a config there governs every rung's -# src/, include/ and gui_lib/ together -- a narrow suppression in the shape -# that quietly becomes a wide one. +# choice is which directory. One up, at `examples/`, is refused: a config there +# governs every rung's src/, include/ and gui_lib/ together -- a narrow +# suppression in the shape that quietly becomes a wide one. Nothing gates that +# placement, so it is a rule to keep by hand. # # `examples/lims/include/` is this rung's own header tree. It holds 15 .hpp # and no .cpp at all, so this file governs declarations and nothing else, and @@ -70,7 +69,7 @@ # (`include/morph/.*`) -- so a `TableName` reached from some .cpp is # discarded as non-user code whatever this file says. The case that reaches # CI is clang-tidy-diff analysing a *changed header* as a main file, which is -# the only way any of these declarations has ever been judged (morph#677). +# the only way any of these declarations is judged at all. # # Reach on this revision: 9 of the repository's 71 `static constexpr # std::string_view TableName` declarations are under this directory, all nine diff --git a/examples/lims/tests/.clang-tidy b/examples/lims/tests/.clang-tidy index b1496f1c0..3a3c851de 100644 --- a/examples/lims/tests/.clang-tidy +++ b/examples/lims/tests/.clang-tidy @@ -16,7 +16,7 @@ # a workstation with a current Catch2. That number is checked rather than # asserted: scripts/check_catch2_pin.sh reads the runner's installed version # back off the include path and fails this line and the clang-tidy-diff job -# together if the package moves (morph#666). +# together if the package moves. # # Directory-scoped, and no wider. clang-tidy resolves configuration by walking # up from the file it is analysing and offers no finer granularity than a @@ -28,9 +28,8 @@ # entry is subtracted, and only here. # # One copy per test directory rather than one shared file, because no directory -# contains every example's tests and nothing else. A gate used to fail if a test -# directory on the application ladder had no such file; it was removed with the -# meta-gates, so a new rung now needs this file added by hand. +# contains every example's tests and nothing else. Nothing gates the file's +# presence, so a new rung needs this file added by hand. # readability-function-cognitive-complexity: the check stays ON here. What is # turned off is its counting of increments that come from *inside* a macro @@ -38,10 +37,10 @@ # assertion macro expands to `do { try { ... } catch (...) { ... } } while (...)` # and the check scores that at 4 whatever it is asserting. Seven assertions is # 28 against a threshold of 25, so a TEST_CASE goes over the line for having -# assertions in it (morph#778). +# assertions in it. # -# `IgnoreMacros: true` subtracts exactly that and nothing else. Measured on -# 68a30bcc with clang-tidy 22.1.8 over the 751 translation units the +# `IgnoreMacros: true` subtracts exactly that and nothing else. Measured with +# clang-tidy 22.1.8 over the 751 translation units the # clang-tidy-diff job's configure produces, the check forced on everywhere and # the only variable this option: # diff --git a/examples/pastebin/include/.clang-tidy b/examples/pastebin/include/.clang-tidy index 1e13315e2..7e2fb2380 100644 --- a/examples/pastebin/include/.clang-tidy +++ b/examples/pastebin/include/.clang-tidy @@ -4,9 +4,9 @@ # not a check, not a declaration kind, not a directory's worth of code. # # The long form of this argument, and the probe runs behind every claim below, -# are in examples/bank/include/.clang-tidy, landed by morph#697. This file is -# the same key applied to one more header tree (morph#702), with this rung's -# own numbers rather than bank's. The short form: +# are in examples/bank/include/.clang-tidy. This file is the same key applied to +# one more header tree, with this rung's own numbers rather than bank's. The +# short form: # # * The repository-root .clang-tidy sets # `readability-identifier-naming.VariableCase: camelBack`, and a Lightweight @@ -51,18 +51,17 @@ # clang-tidy *replaces* the root configuration rather than extending it. # Measured on the same probe with the key removed and nothing else changed: # `Error: no checks enabled.`, exit 1, both from a direct clang-tidy run -# and through clang-tidy-diff.py -- which is louder than -# examples/bank/include/.clang-tidy records for the same mutation -# (morph#716). +# and through clang-tidy-diff.py. So its absence is a red build with a +# message naming the problem, not a quietly unlinted directory. # # ── Scope, and why it is this directory ────────────────────────────────────── # # clang-tidy resolves configuration by walking up from the file it is # analysing and offers no finer granularity than a directory, so the only -# choice is which directory. One up, at `examples/`, was refused outright by a -# gate (since removed), because a config there governs every rung's -# src/, include/ and gui_lib/ together -- a narrow suppression in the shape -# that quietly becomes a wide one. +# choice is which directory. One up, at `examples/`, is refused: a config there +# governs every rung's src/, include/ and gui_lib/ together -- a narrow +# suppression in the shape that quietly becomes a wide one. Nothing gates that +# placement, so it is a rule to keep by hand. # # `examples/pastebin/include/` is this rung's own header tree. It holds 9 # .hpp and no .cpp at all, so this file governs declarations and nothing @@ -70,7 +69,7 @@ # (`include/morph/.*`) -- so a `TableName` reached from some .cpp is # discarded as non-user code whatever this file says. The case that reaches # CI is clang-tidy-diff analysing a *changed header* as a main file, which is -# the only way any of these declarations has ever been judged (morph#677). +# the only way any of these declarations is judged at all. # # Reach on this revision: 1 of the repository's 71 `static constexpr # std::string_view TableName` declarations are under this directory, the diff --git a/examples/pastebin/tests/.clang-tidy b/examples/pastebin/tests/.clang-tidy index b1496f1c0..3a3c851de 100644 --- a/examples/pastebin/tests/.clang-tidy +++ b/examples/pastebin/tests/.clang-tidy @@ -16,7 +16,7 @@ # a workstation with a current Catch2. That number is checked rather than # asserted: scripts/check_catch2_pin.sh reads the runner's installed version # back off the include path and fails this line and the clang-tidy-diff job -# together if the package moves (morph#666). +# together if the package moves. # # Directory-scoped, and no wider. clang-tidy resolves configuration by walking # up from the file it is analysing and offers no finer granularity than a @@ -28,9 +28,8 @@ # entry is subtracted, and only here. # # One copy per test directory rather than one shared file, because no directory -# contains every example's tests and nothing else. A gate used to fail if a test -# directory on the application ladder had no such file; it was removed with the -# meta-gates, so a new rung now needs this file added by hand. +# contains every example's tests and nothing else. Nothing gates the file's +# presence, so a new rung needs this file added by hand. # readability-function-cognitive-complexity: the check stays ON here. What is # turned off is its counting of increments that come from *inside* a macro @@ -38,10 +37,10 @@ # assertion macro expands to `do { try { ... } catch (...) { ... } } while (...)` # and the check scores that at 4 whatever it is asserting. Seven assertions is # 28 against a threshold of 25, so a TEST_CASE goes over the line for having -# assertions in it (morph#778). +# assertions in it. # -# `IgnoreMacros: true` subtracts exactly that and nothing else. Measured on -# 68a30bcc with clang-tidy 22.1.8 over the 751 translation units the +# `IgnoreMacros: true` subtracts exactly that and nothing else. Measured with +# clang-tidy 22.1.8 over the 751 translation units the # clang-tidy-diff job's configure produces, the check forced on everywhere and # the only variable this option: # diff --git a/examples/polls/include/.clang-tidy b/examples/polls/include/.clang-tidy index 0e3abfde1..160328e41 100644 --- a/examples/polls/include/.clang-tidy +++ b/examples/polls/include/.clang-tidy @@ -4,9 +4,9 @@ # not a check, not a declaration kind, not a directory's worth of code. # # The long form of this argument, and the probe runs behind every claim below, -# are in examples/bank/include/.clang-tidy, landed by morph#697. This file is -# the same key applied to one more header tree (morph#702), with this rung's -# own numbers rather than bank's. The short form: +# are in examples/bank/include/.clang-tidy. This file is the same key applied to +# one more header tree, with this rung's own numbers rather than bank's. The +# short form: # # * The repository-root .clang-tidy sets # `readability-identifier-naming.VariableCase: camelBack`, and a Lightweight @@ -51,18 +51,17 @@ # clang-tidy *replaces* the root configuration rather than extending it. # Measured on the same probe with the key removed and nothing else changed: # `Error: no checks enabled.`, exit 1, both from a direct clang-tidy run -# and through clang-tidy-diff.py -- which is louder than -# examples/bank/include/.clang-tidy records for the same mutation -# (morph#716). +# and through clang-tidy-diff.py. So its absence is a red build with a +# message naming the problem, not a quietly unlinted directory. # # ── Scope, and why it is this directory ────────────────────────────────────── # # clang-tidy resolves configuration by walking up from the file it is # analysing and offers no finer granularity than a directory, so the only -# choice is which directory. One up, at `examples/`, was refused outright by a -# gate (since removed), because a config there governs every rung's -# src/, include/ and gui_lib/ together -- a narrow suppression in the shape -# that quietly becomes a wide one. +# choice is which directory. One up, at `examples/`, is refused: a config there +# governs every rung's src/, include/ and gui_lib/ together -- a narrow +# suppression in the shape that quietly becomes a wide one. Nothing gates that +# placement, so it is a rule to keep by hand. # # `examples/polls/include/` is this rung's own header tree. It holds 11 .hpp # and no .cpp at all, so this file governs declarations and nothing else, and @@ -70,7 +69,7 @@ # (`include/morph/.*`) -- so a `TableName` reached from some .cpp is # discarded as non-user code whatever this file says. The case that reaches # CI is clang-tidy-diff analysing a *changed header* as a main file, which is -# the only way any of these declarations has ever been judged (morph#677). +# the only way any of these declarations is judged at all. # # Reach on this revision: 6 of the repository's 71 `static constexpr # std::string_view TableName` declarations are under this directory, all six diff --git a/examples/polls/tests/.clang-tidy b/examples/polls/tests/.clang-tidy index b1496f1c0..3a3c851de 100644 --- a/examples/polls/tests/.clang-tidy +++ b/examples/polls/tests/.clang-tidy @@ -16,7 +16,7 @@ # a workstation with a current Catch2. That number is checked rather than # asserted: scripts/check_catch2_pin.sh reads the runner's installed version # back off the include path and fails this line and the clang-tidy-diff job -# together if the package moves (morph#666). +# together if the package moves. # # Directory-scoped, and no wider. clang-tidy resolves configuration by walking # up from the file it is analysing and offers no finer granularity than a @@ -28,9 +28,8 @@ # entry is subtracted, and only here. # # One copy per test directory rather than one shared file, because no directory -# contains every example's tests and nothing else. A gate used to fail if a test -# directory on the application ladder had no such file; it was removed with the -# meta-gates, so a new rung now needs this file added by hand. +# contains every example's tests and nothing else. Nothing gates the file's +# presence, so a new rung needs this file added by hand. # readability-function-cognitive-complexity: the check stays ON here. What is # turned off is its counting of increments that come from *inside* a macro @@ -38,10 +37,10 @@ # assertion macro expands to `do { try { ... } catch (...) { ... } } while (...)` # and the check scores that at 4 whatever it is asserting. Seven assertions is # 28 against a threshold of 25, so a TEST_CASE goes over the line for having -# assertions in it (morph#778). +# assertions in it. # -# `IgnoreMacros: true` subtracts exactly that and nothing else. Measured on -# 68a30bcc with clang-tidy 22.1.8 over the 751 translation units the +# `IgnoreMacros: true` subtracts exactly that and nothing else. Measured with +# clang-tidy 22.1.8 over the 751 translation units the # clang-tidy-diff job's configure produces, the check forced on everywhere and # the only variable this option: # From d0609c7183ab2e8f215d65c708db158e730cb079 Mon Sep 17 00:00:00 2001 From: Yaraslau Tamashevich Date: Thu, 24 Sep 2026 02:54:18 +0200 Subject: [PATCH 02/10] comments(examples/common): state the constraint, drop the citation The shared testkit, its GUI helpers and examples/common/CMakeLists.txt. The measured blocks stay whole -- the 165 TSan warnings behind deadline.hpp's Qt-free rule, the 0.9^60 sampling argument in test_action_driver.cpp, the seed sweep that found 314/779/2522, the clang-tidy reach argument in test_db_fixture.cpp. What goes is the ticket numbers, the dates of gates that have since been removed, and the paragraphs written as "this used to ..." -- each restated as the constraint that is true now. One user-visible string changed: `PoolTransactionAudit`'s leak diagnostic opened with `morph#740:` and now opens with `PoolTransactionAudit:`. Its only reader is the audit's own self-test, whose `contains("morph#740")` moves to `contains("PoolTransactionAudit")` in the same commit; the rest of the message, including the `SQL_ATTR_AUTOCOMMIT still OFF` phrase that test also asserts on, is untouched. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW --- examples/common/CMakeLists.txt | 26 ++++++++--------- examples/common/db/pool_transaction_audit.cpp | 9 +++--- examples/common/db/pool_transaction_audit.hpp | 4 +-- examples/common/gui/error_text.hpp | 2 +- examples/common/gui/id_qml.hpp | 6 ++-- examples/common/gui/presenter.hpp | 4 +-- examples/common/testkit/backend_rig.hpp | 14 ++++----- examples/common/testkit/db_fixture.hpp | 8 ++--- examples/common/testkit/deadline.hpp | 17 +++++------ examples/common/testkit/qml_surface.hpp | 3 +- examples/common/testkit/step_executor.hpp | 4 +-- .../common/testkit/strand_interleaver.hpp | 10 +++---- .../common/testkit/test_action_driver.cpp | 17 +++++------ .../common/testkit/test_db_busy_fixture.cpp | 2 +- examples/common/testkit/test_db_fixture.cpp | 29 +++++++++---------- examples/common/testkit/test_error_text.cpp | 10 +++---- examples/common/testkit/test_event_poller.cpp | 11 ++++--- examples/common/testkit/test_id_qml.cpp | 20 ++++++------- .../testkit/test_pool_transaction_audit.cpp | 8 ++--- examples/common/testkit/test_presenter.cpp | 17 +++++------ examples/common/testkit/test_pump.cpp | 4 +-- examples/common/testkit/test_qml_surface.cpp | 4 +-- 22 files changed, 107 insertions(+), 122 deletions(-) diff --git a/examples/common/CMakeLists.txt b/examples/common/CMakeLists.txt index d06dfb305..75e6c48da 100644 --- a/examples/common/CMakeLists.txt +++ b/examples/common/CMakeLists.txt @@ -159,7 +159,8 @@ unset(_morph_saved_skip_install_rules) # (target_compile_options(Lightweight PUBLIC ${ODBC_CFLAGS}), its # CMakeLists.txt at the pinned SHA), so it reaches every consumer as a plain # `-I` — which clang coverage-instruments, unlike an `-isystem` one. This -# target is on the coverage path; morph#438 is the failing gate. See +# target is on the coverage path, so those instrumented third-party headers +# would land in the coverage report and move its numbers. See # cmake/morph_demote_interface_includes.cmake for the whole chain and for why # the fix belongs here rather than on each of the five consumers. include(${PROJECT_SOURCE_DIR}/cmake/morph_demote_interface_includes.cmake) @@ -182,18 +183,17 @@ endif() # this target (see the note below on Lightweight). # # Those two TUs live in testkit_src/ rather than beside their headers in -# testkit/, and that is the whole of what morph#652 fixed. clang-tidy resolves -# its configuration from the path of the translation unit it is analysing, and +# testkit/, and the reason is clang-tidy's configuration resolution: it reads +# the configuration from the path of the translation unit it is analysing, and # testkit/.clang-tidy subtracts bugprone-chained-comparison on the argument # that the finding is Catch2's REQUIRE expansion rather than a defect. That # argument is true of the twenty-one Catch2 sources in that directory and says -# nothing about a library TU with no REQUIRE in it -- yet while these two sat -# there, the suppression covered them on the same terms, and the file's own -# prose claimed it could not ("a hand-written `a < b < c` in the code under -# test is still reported"). One directory up the suppression cannot reach -# them, so the claim is true by construction instead of by assertion. The -# headers stay in testkit/ because every consumer includes them as -# ; only the two TUs moved. +# nothing about a library TU with no REQUIRE in it. Placed in testkit/ these two +# would inherit the suppression on those terms anyway, contradicting the file's +# own prose ("a hand-written `a < b < c` in the code under test is still +# reported"); one directory up it cannot reach them, so the claim holds by +# construction rather than by assertion. The headers stay in testkit/ because +# every consumer includes them as ; only the TUs are separated. # # Which is why the two headers are listed as sources below, and they are not # there to be compiled. AUTOMOC finds a Q_OBJECT header two ways: next to the @@ -335,6 +335,6 @@ catch_discover_tests(ladder_common_tests # whose name contains a `;` is flattened into two fragments that name no test, # and set_tests_properties silently applies to nothing. The case then keeps # whatever label discovery gave it and never gains the one the pass exists to -# add. That was morph#173, found because `ctest -L ladder-` counted fewer -# tests than the binary reported. Applying the label at discovery, above, -# removes the mechanism rather than hardening it. +# add. The symptom is `ctest -L ladder-` counting fewer tests than the +# binary reports. Applying the label at discovery, above, removes the mechanism +# rather than hardening it. diff --git a/examples/common/db/pool_transaction_audit.cpp b/examples/common/db/pool_transaction_audit.cpp index 5b01611f4..6196c5727 100644 --- a/examples/common/db/pool_transaction_audit.cpp +++ b/examples/common/db/pool_transaction_audit.cpp @@ -30,8 +30,7 @@ /// It is deliberately **not** a Catch2 translation unit and deliberately not /// in `examples/common/testkit/`: that directory's `.clang-tidy` subtracts /// `bugprone-chained-comparison` on an argument about Catch2's `REQUIRE` -/// expansion, a gate removed on 2026-09-23 enforces that every `.cpp` -/// under it really is a Catch2 source, and morph#652 is the record of what -/// happened the last time library code sat there. The audit's behavioural -/// tests live in `testkit/test_pool_transaction_audit.cpp`, where they -/// belong. +/// expansion, and a library .cpp placed there would inherit that subtraction +/// on the strength of an argument that does not apply to it. Nothing enforces +/// the split, so it is kept by hand. The audit's behavioural tests live in +/// `testkit/test_pool_transaction_audit.cpp`, where they belong. diff --git a/examples/common/db/pool_transaction_audit.hpp b/examples/common/db/pool_transaction_audit.hpp index 33e150cdd..291e37400 100644 --- a/examples/common/db/pool_transaction_audit.hpp +++ b/examples/common/db/pool_transaction_audit.hpp @@ -13,7 +13,7 @@ /// @file /// A process-wide check that no pooled `Lightweight::DataMapper` is ever /// returned to `Lightweight::GlobalDataMapperPool()` with a transaction still -/// open on its connection (morph#740). +/// open on its connection. /// /// @par The defect this exists to catch /// `Lightweight::Pool::Return` (`src/Lightweight/DataMapper/Pool.hpp` @@ -163,7 +163,7 @@ class PoolTransactionAudit : public ::Lightweight::SqlLogger::Null { return; } ++_detections; - std::string message = "morph#740: a pooled DataMapper was "; + std::string message = "PoolTransactionAudit: a pooled DataMapper was "; message += what; message += " with SQL_ATTR_AUTOCOMMIT still OFF -- a transaction is open on it. " diff --git a/examples/common/gui/error_text.hpp b/examples/common/gui/error_text.hpp index a71f52237..b5f345615 100644 --- a/examples/common/gui/error_text.hpp +++ b/examples/common/gui/error_text.hpp @@ -13,7 +13,7 @@ /// which signal carries the result. `docs/analysis/qml-bridge-boilerplate.md` /// measured the rest of the bridge duplication as O(QML surface) and largely /// irreducible; this is one of the two pieces it identified as genuinely -/// model-independent and worth extracting (morph#168). +/// model-independent and worth extracting. namespace morph::ladder::gui { diff --git a/examples/common/gui/id_qml.hpp b/examples/common/gui/id_qml.hpp index e57592e62..247b66512 100644 --- a/examples/common/gui/id_qml.hpp +++ b/examples/common/gui/id_qml.hpp @@ -14,7 +14,7 @@ /// back. `docs/analysis/qml-bridge-boilerplate.md` measured the rest of the /// bridge duplication as O(QML surface) and largely irreducible; this is the /// second of the two pieces it identified as genuinely model-independent and -/// worth extracting (morph#169), after `errorText()` (morph#168). +/// worth extracting; `errorText()` is the other. /// /// @par What an "id" is here /// Nothing in this header names a model type. It works on anything with @@ -37,8 +37,8 @@ /// only thing that is true of both: **whatever the id calls empty maps to the /// empty representation, and everything the id calls engaged — `0` included — /// maps to its own payload.** For an optional-backed id that is exactly the -/// unset-vs-zero distinction morph#169 asks for; for a zero-sentinel id it is -/// the strongest statement that is not a lie. +/// unset-vs-zero distinction a QML row has to preserve; for a zero-sentinel id +/// it is the strongest statement that is not a lie. namespace morph::ladder::gui { diff --git a/examples/common/gui/presenter.hpp b/examples/common/gui/presenter.hpp index 2b7d1abfc..9b56b7ad1 100644 --- a/examples/common/gui/presenter.hpp +++ b/examples/common/gui/presenter.hpp @@ -121,8 +121,8 @@ class Presenter : public QObject { // from that rig) is destroyed *before* it, and `BackendRig`'s // destructor then deliberately pumps the Qt event loop to flush // queued posts — resolving completions into a presenter that is - // already gone. AddressSanitizer caught that as a - // `stack-use-after-scope` write in `finishOne()` (morph#137). + // already gone. Without the guard AddressSanitizer reports that as a + // `stack-use-after-scope` write in `finishOne()`. // // The guard covers `onOk`/`onErr` as well as `finishOne()`: a // subclass's callback captures *its* `this`, so running it against a diff --git a/examples/common/testkit/backend_rig.hpp b/examples/common/testkit/backend_rig.hpp index c52c9ed58..e6b43b7de 100644 --- a/examples/common/testkit/backend_rig.hpp +++ b/examples/common/testkit/backend_rig.hpp @@ -314,14 +314,12 @@ class BackendRig { // including a nested one chained from inside another posted callback // -- has already happened by the time the drain below runs. _workerPool.reset(); - // No event-loop drain here any more. This used to spin - // processEvents() for a fixed number of slices to flush posts queued - // by those tasks before any client executor was freed -- a workaround - // for morph#127, where a queued task delivered after its QtExecutor - // died called post() on the freed executor. `QtExecutor` now drops a - // task whose executor is already gone, so the hazard is closed in the - // framework rather than worked around in this one fixture. A fixed - // slice count was never a proof anyway, only a "probably enough". + // No event-loop drain here, and none is needed: a post() queued by + // one of those tasks and delivered after its QtExecutor has died is + // dropped by `QtExecutor` itself, so the hazard is closed in the + // framework rather than worked around in this fixture. Spinning + // processEvents() for some number of slices would not be a proof of + // anything either, only a "probably enough". } [[nodiscard]] Mode mode() const { return _mode; } diff --git a/examples/common/testkit/db_fixture.hpp b/examples/common/testkit/db_fixture.hpp index 09d4b51c6..0fd16a2ec 100644 --- a/examples/common/testkit/db_fixture.hpp +++ b/examples/common/testkit/db_fixture.hpp @@ -159,7 +159,7 @@ class DbFixture { /// no branch of its own left to miss. static void ensureConnectionConfigured() { static const bool once = [] { - // morph#740: installed here as well as in every rung's own + // Installed here as well as in every rung's own // `db::setup()`/`db::configure()`, because no ladder test goes // through those -- this fixture points Lightweight at the test // database itself. Without it the audit would be live only in @@ -355,15 +355,15 @@ class DbFixture { /// file — empties it first, so that the tests after this one are /// not reported against it. /// - /// ── Why it both repairs *and* fails (morph#766) ──────────────────────── + /// ── Why it both repairs *and* fails ──────────────────────────────────── /// /// The database is one real file (`morph_ladder_test.db` by default), /// shared by every test in the binary and kept between runs by design — /// see this file's `@file` comment. So a state the drop sweep cannot /// handle is not one test's problem: it is every later test's, in this run /// and in every run after it, until somebody deletes a file that nothing - /// told them about. That was morph#766's expensive half. The cheap half - /// was that the throw said only `map::at`, attributed to the `TEST_CASE` + /// told them about. That is the expensive half of the failure; the cheap + /// half is a throw that says only `map::at`, attributed to the `TEST_CASE` /// line, naming neither the fixture, nor the file, nor the fact that the /// state is on disk at all. /// diff --git a/examples/common/testkit/deadline.hpp b/examples/common/testkit/deadline.hpp index 11819a8ab..6cbbf5301 100644 --- a/examples/common/testkit/deadline.hpp +++ b/examples/common/testkit/deadline.hpp @@ -15,15 +15,14 @@ /// offers is a *Qt event-loop* wait: it includes `` and /// calls `processEvents`. That include is the whole obstacle. kanban's /// concurrent-move stress test (`examples/kanban/tests/test_kanban_stress.cpp`) -/// is deliberately Qt-free — morph#128 catalogued 165 ThreadSanitizer warnings -/// that all bottomed out in Qt-internal frames reached through the -/// `QtExecutor` its earlier, `pumpUntil`-driven version pulled in, and a -/// prebuilt Qt cannot be seen through by TSan, making those warnings unusable -/// evidence either way. That file therefore owns a small `waitUntil` of its -/// own over `std::this_thread::sleep_for`, and until this header existed it -/// was the last unscaled wait poll in `examples/`: its budgets were fixed -/// wall-clock constants, so `MORPH_LADDER_DEADLINE_MS` moved every deadline -/// in the ladder except the ones in the slowest test. +/// is deliberately Qt-free: a `pumpUntil`-driven version of it pulls in +/// `QtExecutor`, and a run under ThreadSanitizer then produces 165 warnings +/// that all bottom out in Qt-internal frames — a prebuilt Qt cannot be seen +/// through by TSan, so those warnings are unusable evidence either way. That +/// file therefore owns a small `waitUntil` of its own over +/// `std::this_thread::sleep_for`, and without this header its budgets would be +/// fixed wall-clock constants: `MORPH_LADDER_DEADLINE_MS` would move every +/// deadline in the ladder except the ones in the slowest test. /// /// Splitting the scale factor out is what lets both hold: `pump.hpp` includes /// this header and is otherwise unchanged, so every existing caller keeps the diff --git a/examples/common/testkit/qml_surface.hpp b/examples/common/testkit/qml_surface.hpp index 253a39245..c40aad777 100644 --- a/examples/common/testkit/qml_surface.hpp +++ b/examples/common/testkit/qml_surface.hpp @@ -11,8 +11,7 @@ /// @file /// The metaobject-surface drift guard, generalised out of the hand-written -/// one `examples/bookmarks/tests/test_bookmark_qml_bridges.cpp` established -/// (morph#86's "what is actually justified", third bullet). +/// one `examples/bookmarks/tests/test_bookmark_qml_bridges.cpp` established. /// /// @par What drifts, and why nothing else catches it /// A rung's QML binds its bridges *by string*: `page.tagController.refresh()` diff --git a/examples/common/testkit/step_executor.hpp b/examples/common/testkit/step_executor.hpp index 4d893e141..68fd4178f 100644 --- a/examples/common/testkit/step_executor.hpp +++ b/examples/common/testkit/step_executor.hpp @@ -22,8 +22,8 @@ /// `DeterministicExecutor` is duplicated from that same header -- /// `tests/test_support.hpp` is a private header for `morph_tests`' own /// translation units and has no reachable include path from `examples/`. -/// That reachability gap, not the absence of the semantics, is what left -/// every ladder async-job test spinning a real pool (morph#161). +/// That reachability gap, not the absence of the semantics, is why a ladder +/// async-job test has to reach for this copy instead of spinning a real pool. namespace morph::ladder::testkit { diff --git a/examples/common/testkit/strand_interleaver.hpp b/examples/common/testkit/strand_interleaver.hpp index 55d89f613..480c358e9 100644 --- a/examples/common/testkit/strand_interleaver.hpp +++ b/examples/common/testkit/strand_interleaver.hpp @@ -21,12 +21,10 @@ /// `morph::exec::detail::ModelId`s and name both directly — the production /// components whose per-key ordering guarantee is the point of this harness. /// A stand-in would prove nothing here: unlike `morph::testing::StepExecutor` -/// (issue #55's public seam, used elsewhere to interleave `RemoteServer` -/// dispatch *without* naming `StrandExecutor`), these particular tests exist -/// to test `StrandExecutor` itself. This is a deliberate, accepted -/// testkit-layer reach-in into a `detail::` namespace, not a gap awaiting a -/// public seam — see the historical discussion in -/// https://github.com/LASTRADA-Software/morph/issues/55. +/// (a public seam, used elsewhere to interleave `RemoteServer` dispatch +/// *without* naming `StrandExecutor`), these particular tests exist to test +/// `StrandExecutor` itself. This is a deliberate, accepted testkit-layer +/// reach-in into a `detail::` namespace, not a gap awaiting a public seam. namespace morph::ladder::testkit { diff --git a/examples/common/testkit/test_action_driver.cpp b/examples/common/testkit/test_action_driver.cpp index af3452140..cf76c2731 100644 --- a/examples/common/testkit/test_action_driver.cpp +++ b/examples/common/testkit/test_action_driver.cpp @@ -176,15 +176,14 @@ TEST_CASE("SeededScript can pick the last generator, which absorbs the leftover // here -- an early break and a run to completion -- so neither goes // unexercised. // - // Each arm is made *certain* rather than sampled (morph#375). This case - // used to run one 1:9 script for 60 draws and assert that both values - // appeared, which is a property of the sample and not of the walk: the - // light generator is missed with probability 0.9^60 = 1.8e-3. The seed is - // not fixed either -- SeededScript::resolveSeed lets MORPH_STRESS_SEED - // replace every script's seed process-wide, and TESTING.md documents that - // variable as the knob for re-running the ladder *stress* suites, so - // exporting it re-rolled that lottery for this unit case. A sweep of - // seeds 1..3000 found 314, 779 and 2522 failing. + // Each arm is made *certain* rather than sampled. Sampling it -- one 1:9 + // script run for 60 draws, asserting that both values appear -- is a + // property of the sample and not of the walk: the light generator is missed + // with probability 0.9^60 = 1.8e-3, and the seed is not fixed either, since + // SeededScript::resolveSeed lets MORPH_STRESS_SEED replace every script's + // seed process-wide and TESTING.md documents that variable as the knob for + // re-running the ladder *stress* suites. In the sampled form a sweep of + // seeds 1..3000 failed on 314, 779 and 2522. // // Weighting the other generator 0 collapses the draw range to a single // value: _totalWeight is 1, so `uniform_int_distribution{0, 0}` diff --git a/examples/common/testkit/test_db_busy_fixture.cpp b/examples/common/testkit/test_db_busy_fixture.cpp index aeb97f359..8befcbdd3 100644 --- a/examples/common/testkit/test_db_busy_fixture.cpp +++ b/examples/common/testkit/test_db_busy_fixture.cpp @@ -22,7 +22,7 @@ struct BusyProbe { // `tableName` would compile, link, and silently map this record to a table // named after the C++ type — see test_db_fixture.cpp's identical comment on // `LadderTestkitProbe` for the full explanation, including why this is a - // per-declaration directive rather than a directory .clang-tidy (morph#702). + // per-declaration directive rather than a directory .clang-tidy. // NOLINTNEXTLINE(readability-identifier-naming) static constexpr std::string_view TableName = "busy_fixture_probe"; diff --git a/examples/common/testkit/test_db_fixture.cpp b/examples/common/testkit/test_db_fixture.cpp index c45af1ab6..13fae12bd 100644 --- a/examples/common/testkit/test_db_fixture.cpp +++ b/examples/common/testkit/test_db_fixture.cpp @@ -35,7 +35,7 @@ struct LadderTestkitProbe { // 'TableName'` with `tableName` offered as the fix. That rename compiles // and links: the `requires` clause goes false, the `else` branch runs, and // this record silently maps to a table called "LadderTestkitProbe" that no - // migration ever creates. Suppressed rather than taken (morph#702). + // migration ever creates. Suppressed rather than taken. // // Per declaration, and deliberately not a directory .clang-tidy. // examples/bank/include/.clang-tidy exempts the same spelling with @@ -43,17 +43,14 @@ struct LadderTestkitProbe { // and its own reasoning is what argues against copying it here: that file // covers twelve declarations in a header-only tree, reached as main files // and nothing else. This directory is the opposite shape. It holds - // twenty-one Catch2 translation units (the count - // a gate removed on 2026-09-23 reads back) and exactly two of these + // twenty-one Catch2 translation units and exactly two of these // declarations, and a .clang-tidy here is resolved for every one of them — // including for the include/morph/** headers they reach, which *do* match - // the root `HeaderFilterRegex` (morph#632). It also already carries a - // .clang-tidy whose whole justification is "this finding is Catch2 idiom", - // a claim a gate removed on 2026-09-23 re-checks against every .cpp that - // file governs; `TableName` is ORM protocol rather than Catch2 idiom, so - // adding it there would put a second claim into a file whose gate - // validates only the first. Two directives subtract one check on one line - // each and reach nothing else. + // the root `HeaderFilterRegex`. It also already carries a .clang-tidy whose + // whole justification is "this finding is Catch2 idiom"; `TableName` is ORM + // protocol rather than Catch2 idiom, so adding it there would put a second, + // unrelated claim into a file that argues only the first. Two directives + // subtract one check on one line each and reach nothing else. // NOLINTNEXTLINE(readability-identifier-naming) static constexpr std::string_view TableName = "ladder_testkit_probe"; @@ -118,17 +115,17 @@ TEST_CASE("DbFixture::computeConnectionString uses ODBC_CONNECTION_STRING verbat "DRIVER=PostgreSQL;Database=whatever"); } -// morph#766: when the shared database holds a foreign key whose target table -// is gone, Lightweight's SqlSchema::ReadAllTables throws +// When the shared database holds a foreign key whose target table is gone, +// Lightweight's SqlSchema::ReadAllTables throws // std::out_of_range("map::at") out of DbFixture's constructor — before // anything has been dropped, so the bad state survives the run that reported // it and every test after it fails the same way, across invocations, until // somebody deletes a file nothing names. // -// Two test cases below, because the issue has two halves and either can be -// fixed without the other: the message has to name the fixture, the file and -// the remedy, *and* the database has to be usable afterwards. A fix that only -// improved the message would pass the first and fail the second. +// Two test cases below, because the requirement has two halves and either can +// hold without the other: the message has to name the fixture, the file and +// the remedy, *and* the database has to be usable afterwards. A fixture that +// only improved the message would satisfy the first and fail the second. namespace { /// @brief Leaves the shared database holding a foreign key whose target table diff --git a/examples/common/testkit/test_error_text.cpp b/examples/common/testkit/test_error_text.cpp index 50df8294f..15c779b68 100644 --- a/examples/common/testkit/test_error_text.cpp +++ b/examples/common/testkit/test_error_text.cpp @@ -1,11 +1,11 @@ // SPDX-License-Identifier: Apache-2.0 // -// Tests for morph::ladder::gui::errorText (morph#168). +// Tests for morph::ladder::gui::errorText. // -// The extraction replaced 13 hand-written copies across six rungs. Most of -// them lacked a `catch (...)` arm, so this is not a pure refactor -- the -// behaviour for a non-std::exception throw changes from "escapes the callback" -// to "returns text". These pin both the preserved behaviour and the fixed one. +// `errorText()` is the one copy of the conversion six rungs used to hand-write, +// and it has a `catch (...)` arm, which is the part a hand-written copy most +// often omits: a non-std::exception throw returns text rather than escaping the +// callback. These pin both arms. #include #include diff --git a/examples/common/testkit/test_event_poller.cpp b/examples/common/testkit/test_event_poller.cpp index eb6b6c27b..63ef03963 100644 --- a/examples/common/testkit/test_event_poller.cpp +++ b/examples/common/testkit/test_event_poller.cpp @@ -703,16 +703,15 @@ TEST_CASE("EventPoller tolerates an empty onFatalError callback on a non-timeout } // ═════════════════════════════════════════════════════════════════════════ -// The two failure-classifier helpers, called directly (morph#411) +// The two failure-classifier helpers, called directly // ═════════════════════════════════════════════════════════════════════════ // // `isClientTimeout` and `describeFailure` are declared in // `event_poller.hpp`'s `detail` namespace with a documented answer for a null -// `std::exception_ptr` and for a throw that is not a `std::exception`. Both -// answers were reachable only through `handleError`, which never produces -// either shape, so what the header promises was held up by nothing. They are -// free functions taking an `exception_ptr`, so the promise can simply be -// called. +// `std::exception_ptr` and for a throw that is not a `std::exception`. Neither +// shape is reachable through `handleError`, so going through the poller would +// leave those two answers asserted by nothing. They are free functions taking +// an `exception_ptr`, so the promise is called directly instead. TEST_CASE("isClientTimeout: a null exception is not a timeout", "[testkit][event-poller]") { // The distinction this classifier draws decides whether the poller retries diff --git a/examples/common/testkit/test_id_qml.cpp b/examples/common/testkit/test_id_qml.cpp index b3b2a4988..3e427f007 100644 --- a/examples/common/testkit/test_id_qml.cpp +++ b/examples/common/testkit/test_id_qml.cpp @@ -1,11 +1,10 @@ // SPDX-License-Identifier: Apache-2.0 // -// Tests for morph::ladder::gui::idNumber/idText/idFromText (morph#169). +// Tests for morph::ladder::gui::idNumber/idText/idFromText. // -// The extraction replaced hand-written conversions in ten files across five -// rungs. It is a pure refactor -- every converted site kept the exact -// representation it already had -- so what these pin is the invariant that -// made the duplication worth removing rather than any changed behaviour: +// These helpers carry the conversion that ten files across five rungs would +// otherwise hand-write, each keeping the exact representation it needs. So what +// these pin is the invariant that makes one shared copy worth having: // // an id the model calls EMPTY and an id the model calls ENGAGED-WITH-ZERO // must not arrive at QML as the same value. @@ -60,7 +59,7 @@ struct ZeroSentinel { // ── The three states that must stay distinct ──────────────────────────────── TEST_CASE("idNumber keeps unset, zero and an ordinary id apart", "[id-qml]") { - // This is morph#169's whole reason for existing. Written as three + // This is the whole reason these helpers exist. Written as three // pairwise inequalities rather than three equalities, because a // conversion that collapsed unset onto zero would still satisfy any two // of the equalities on their own. @@ -168,11 +167,10 @@ TEST_CASE("idFromText yields an empty id for text that is not a number", "[id-qm TEST_CASE("the text form carries a full-width id exactly", "[id-qml]") { // idText/idFromText are exact across the whole int64 range because the // digits never become a number in between. This is NOT a claim about what - // QML then does with the string: morph#190/#191 found that QML's - // JSON.parse/JSON.stringify round trip rounds anything past 2^53, and - // idNumber's qlonglong reaches QML through the same numeric engine. Fixing - // that belongs to those issues; this only pins that the helper does not - // add a second lossy step of its own. + // QML then does with the string: QML's JSON.parse/JSON.stringify round trip + // rounds anything past 2^53, and idNumber's qlonglong reaches QML through + // the same numeric engine. That loss is above this layer; this only pins + // that the helper does not add a second lossy step of its own. constexpr std::int64_t big = 9007199254740993; // 2^53 + 1 CHECK(idText(OptionalId{big}) == QStringLiteral("9007199254740993")); CHECK(idFromText(idText(OptionalId{big})) == OptionalId{big}); diff --git a/examples/common/testkit/test_pool_transaction_audit.cpp b/examples/common/testkit/test_pool_transaction_audit.cpp index aaa757531..1e180b425 100644 --- a/examples/common/testkit/test_pool_transaction_audit.cpp +++ b/examples/common/testkit/test_pool_transaction_audit.cpp @@ -10,8 +10,8 @@ #include "db/pool_transaction_audit.hpp" #include "testkit/db_fixture.hpp" -// morph#740's acceptance. The claim under test is not "the suite passes" -- -// it is that a mapper deliberately returned to the pool mid-transaction is +// The audit's own acceptance. The claim under test is not "the suite passes" +// -- it is that a mapper deliberately returned to the pool mid-transaction is // named by the audit, and that the audit is silent when nothing leaks. Both // directions are asserted below, because a check that never fires and a check // that always fires are indistinguishable from one that measures nothing. @@ -50,7 +50,7 @@ TEST_CASE("autocommitStateOf reports what SqlTransaction does to a connection", } CHECK(autocommitStateOf(mapper->Connection()) == AutocommitState::On); - // And the correction morph#566's rescope turned on: Commit() restores + // And the part that is easy to get backwards: Commit() restores // autocommit itself, there and then -- it does not wait for the // destructor. Every statement after an explicit Commit() is therefore // already back in autocommit, however long the SqlTransaction local @@ -104,7 +104,7 @@ TEST_CASE("PoolTransactionAudit names a pooled mapper returned mid-transaction", CHECK(audit.detections() >= 1); REQUIRE_FALSE(collector.messages.empty()); - CHECK(collector.messages.front().contains("morph#740")); + CHECK(collector.messages.front().contains("PoolTransactionAudit")); CHECK(collector.messages.front().contains("SQL_ATTR_AUTOCOMMIT still OFF")); // Roll back before leaving, so the leaked connection is clean again diff --git a/examples/common/testkit/test_presenter.cpp b/examples/common/testkit/test_presenter.cpp index 58092c116..4904c3321 100644 --- a/examples/common/testkit/test_presenter.cpp +++ b/examples/common/testkit/test_presenter.cpp @@ -376,14 +376,13 @@ TEST_CASE("Presenter::trackBound() still emits bound() when the presenter is des TEST_CASE("Presenter::track() does not touch a presenter destroyed before its completion resolves", "[ladder][testkit][gui][presenter]") { - // The track() counterpart of the trackBound() case above, and regression - // coverage for morph#137. track() used to capture a bare `this`, while - // trackBound() -- the method immediately above it in presenter.hpp -- - // already used a QPointer and documented why. A Completion resolves - // through the executor (posted, never delivered inline), so a presenter - // destroyed before that post runs had finishOne() write to freed memory: - // AddressSanitizer reported a stack-use-after-scope on the atomic - // fetch_sub, from a completion flushed by BackendRig's teardown pump. + // The track() counterpart of the trackBound() case above. Both capture a + // `QPointer` rather than a bare `this`, and this case is what holds them to + // it. A Completion resolves through the executor (posted, never delivered + // inline), so a presenter destroyed before that post runs would have + // finishOne() write to freed memory: with a bare `this`, AddressSanitizer + // reports a stack-use-after-scope on the atomic fetch_sub, from a + // completion flushed by BackendRig's teardown pump. // // Constructing the presenter in a nested scope and pumping after it dies // reproduces that exactly. There is nothing to assert but "this does not @@ -549,7 +548,7 @@ TEST_CASE("Presenter::trackBound() emits bound() on the .onError path when regis // tests/qt/test_qt_websocket.cpp's issue26/issue54 cases use, chosen so // this is a real onError delivery rather than a timing race. With // asyncRegistrationEnabled set, constructing the handler queues its - // registration (issue #54's pre-connect queueing); QtWebSocketBackend's + // registration (the backend's pre-connect queueing); QtWebSocketBackend's // own disconnect/never-connected handling then drains that queue through // cancelPending(DisconnectedError), which is whenBound()'s only route to // .onError() -- see qt_websocket_backend.cpp's cancelPending(). diff --git a/examples/common/testkit/test_pump.cpp b/examples/common/testkit/test_pump.cpp index 97c1fd5ad..6b896d145 100644 --- a/examples/common/testkit/test_pump.cpp +++ b/examples/common/testkit/test_pump.cpp @@ -42,8 +42,8 @@ TEST_CASE("computeDeadlineScale is 1.0 for an unparseable value, not a crash", " // morph::async::Completion is consumer-facing only (then()/onError()); it has // no resolve()/fail() of its own. The producer side is Completion:: -// makeSettleable(execPtr) (issue #55's public "settleable promise" seam, -// docs/spec/core/completion.md), which returns a {Completion, Promise} +// makeSettleable(execPtr), the public "settleable promise" seam +// (docs/spec/core/completion.md), which returns a {Completion, Promise} // pair sharing one state -- the Promise exposes resolve()/reject() without // ever naming morph::async::detail::CompletionState. Here we use // morph::qt::QtExecutor (already linked in via morph::qt) as the executor, diff --git a/examples/common/testkit/test_qml_surface.cpp b/examples/common/testkit/test_qml_surface.cpp index e8913eb9d..3120df8c3 100644 --- a/examples/common/testkit/test_qml_surface.cpp +++ b/examples/common/testkit/test_qml_surface.cpp @@ -917,8 +917,8 @@ TEST_CASE("QmlSurfaceAudit: a guarded read of a member the bridge DOES have stil } // ═════════════════════════════════════════════════════════════════════════ -// The arms nothing executed — each one a promise the audit's own header -// makes and, until now, kept only by inspection (morph#411) +// The arms the cases above do not reach — each one a promise the audit's own +// header makes, and which nothing but these cases executes // ═════════════════════════════════════════════════════════════════════════ TEST_CASE("blankCommentsAndStrings blanks single-quoted and template strings, not just double-quoted", From 5301a6cd5a4ca167bbb0a991a1f0db0bd9d4d56c Mon Sep 17 00:00:00 2001 From: Yaraslau Tamashevich Date: Thu, 24 Sep 2026 02:58:29 +0200 Subject: [PATCH 03/10] comments(examples/bank): state the constraint, drop the citation bank's CMakeLists, the GUI controllers and its three test suites. The measurements stay: the 21-of-21 `disk I/O error (10)` under `ctest -j` behind unique_test_database.hpp, `parseMinor`'s 0.49999999999999994 + 0.5 == 1.0 table, and the pool-hand-off count that makes test_pool_scope.cpp's zero evidence rather than an unwired counter. What goes is the ticket numbers and the "before the fix it did X" framing, each restated as the constraint the code holds now. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW --- examples/bank/CMakeLists.txt | 47 +++++++++---------- examples/bank/README.md | 5 +- examples/bank/gui/CMakeLists.txt | 5 +- examples/bank/gui/controllers/Format.hpp | 8 ++-- examples/bank/gui/qml/MoveMoneyPage.qml | 2 +- .../offline/lightweight_offline_queue.hpp | 2 +- examples/bank/tests/bank_test_support.hpp | 20 ++++---- .../bank/tests/gui/test_bank_gui_format.cpp | 22 ++++----- .../tests/gui/test_bank_gui_qml_behaviour.cpp | 30 ++++++------ .../bank/tests/gui/test_bank_qml_surface.cpp | 19 ++++---- .../tests/test_lightweight_offline_queue.cpp | 4 +- examples/bank/tests/test_pool_scope.cpp | 14 +++--- examples/bank/tests/unique_test_database.hpp | 2 +- 13 files changed, 89 insertions(+), 91 deletions(-) diff --git a/examples/bank/CMakeLists.txt b/examples/bank/CMakeLists.txt index 227472622..c51fae737 100644 --- a/examples/bank/CMakeLists.txt +++ b/examples/bank/CMakeLists.txt @@ -66,7 +66,7 @@ unset(_morph_saved_skip_install_rules) # it has to be here too: whichever of the two sites configures first is the one # that actually defines the target, so only calling it from the other would # leave the `-I` in place whenever bank configures first. The helper is -# idempotent, so calling it twice is a no-op the second time (morph#438). +# idempotent, so calling it twice is a no-op the second time. include(${PROJECT_SOURCE_DIR}/cmake/morph_demote_interface_includes.cmake) morph_demote_lightweight_odbc_includes() @@ -95,7 +95,7 @@ apply_bigobj(bank_lib) # Note: deliberately NOT calling apply_warnings() here — the third-party ORM # headers are not -Werror clean and would fail the build. # -# apply_sanitizers() is a separate question and the answer is yes (morph#679). +# apply_sanitizers() is a separate question and the answer is yes. # Until this line, `ladder_bank_server` was the *only* bank target that carried # an AF_SANITIZER block, so a sanitizer configure with # -DMORPH_BUILD_BANK_EXAMPLE=ON built bank_lib, bank_cli and every bank test @@ -210,8 +210,8 @@ if(MORPH_BUILD_TESTS) tests/test_relations.cpp tests/test_stateful_account.cpp # Not a behavioural test of bank: the record of why - # examples/common/db/pool_transaction_audit.hpp is deliberately not - # linked here (morph#752). See the file's own header. + # examples/common/db/pool_transaction_audit.hpp is deliberately + # not linked here. See the file's own header. tests/test_pool_scope.cpp ) # ${PROJECT_SOURCE_DIR}/tests is morph's own test directory, and it is @@ -219,8 +219,8 @@ if(MORPH_BUILD_TESTS) # tests/offline_queue_conformance.hpp, the implementation-agnostic # IOfflineQueue suite every shipped queue is measured against. The # Lightweight-backed queue in this example is measured against the same - # suite, unedited -- that is the whole acceptance gate of morph#549, and - # a copy of the suite next to this example would have measured nothing. + # suite, unedited -- that is the whole point of the conformance suite, + # and a copy of it next to this example would measure nothing. target_include_directories(bank_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/tests ${PROJECT_SOURCE_DIR}/tests) @@ -230,16 +230,15 @@ if(MORPH_BUILD_TESTS) # A ctest command, so this block is not optional: without it the # binary runs on a sanitizer leg carrying nothing, and # scripts/check_sanitizer_instrumentation.sh fails the job rather than - # letting it report a clean run over an uninstrumented suite - # (morph#679). + # letting it report a clean run over an uninstrumented suite. if(DEFINED AF_SANITIZER) apply_sanitizers(bank_tests ${AF_SANITIZER}) endif() list(APPEND CMAKE_MODULE_PATH ${Catch2_DIR}) include(Catch) - # LABELS "bank" on all three bank suites (morph#679). Bank is not a - # rung, so morph_add_rung()'s "ladder"/"ladder-" labels never + # LABELS "bank" on all three bank suites. Bank is not a rung, so + # morph_add_rung()'s "ladder"/"ladder-" labels never # reach it and nothing else distinguished a bank test from a core one # -- which is what a leg that wants to run bank's suites and only # bank's suites needs. Catch2 tags are not ctest labels (no @@ -249,10 +248,10 @@ if(MORPH_BUILD_TESTS) # label that stopped matching fails the leg instead of running nothing. # # No RESOURCE_LOCK on any of the three calls, and that is a decision - # rather than the oversight it was (morph#682). catch_discover_tests - # registers one ctest case per TEST_CASE and ctest runs each as its own - # process; bank's cases all named one fixed SQLite path under - # temp_directory_path() and each deleted it on the way in, so + # rather than an oversight. catch_discover_tests registers one ctest + # case per TEST_CASE and ctest runs each as its own process; when + # bank's cases all named one fixed SQLite path under + # temp_directory_path() and each deleted it on the way in, # `ctest -j 12 -L bank` failed 21 of 21 with # `HY000 (10) - [SQLite]disk I/O error (10)`. The ladder's remedy for # the same hazard is RESOURCE_LOCK morph_ladder_test_db (see @@ -300,9 +299,9 @@ if(MORPH_BUILD_TESTS) # # testkit_src/, not testkit/: the TU sits one directory up # from its own header so that testkit/.clang-tidy's Catch2 - # suppression cannot reach it (morph#652). The - # ${PROJECT_SOURCE_DIR}/examples/common include directory - # below is what resolves its . + # suppression cannot reach it. The + # ${PROJECT_SOURCE_DIR}/examples/common include directory below + # is what resolves its . ${PROJECT_SOURCE_DIR}/examples/common/testkit_src/qml_surface.cpp ) target_include_directories(bank_gui_tests PRIVATE @@ -317,12 +316,12 @@ if(MORPH_BUILD_TESTS) target_compile_definitions(bank_gui_tests PRIVATE MORPH_LADDER_SOURCE_ROOT="${PROJECT_SOURCE_DIR}") apply_bigobj(bank_gui_tests) - # This is the binary that would have caught morph#663: the - # out-of-range double -> int64_t conversion lived in + # This is the binary that catches an out-of-range + # double -> int64_t conversion: it lives in # gui/controllers/Format.hpp, which this suite compiles and drives # directly (tests/gui/test_bank_gui_format.cpp) with Qt6::Core and - # no display. Uninstrumented it exercised the conversion on every - # run and said nothing (morph#679). + # no display. Uninstrumented, the suite exercises the conversion on + # every run and says nothing. if(DEFINED AF_SANITIZER) apply_sanitizers(bank_gui_tests ${AF_SANITIZER}) endif() @@ -341,7 +340,7 @@ if(MORPH_BUILD_TESTS) # # This target exists because some of bank's GUI logic is *in* the # QML and is invisible from C++. MoveMoneyPage's account picker is - # the worked example (morph#296): the controller is entirely + # the worked example: the controller is entirely # self-consistent when driven directly, and the defect is a # ComboBox whose `currentIndex` no binding restores after its model # is replaced. Only a live engine over the shipped .qml file can @@ -385,7 +384,7 @@ if(MORPH_BUILD_TESTS) MORPH_LADDER_TESTKIT_GUI_APP) apply_bigobj(bank_gui_qml_tests) # Also a ctest command, so also not optional -- see bank_tests - # above (morph#679). + # above. if(DEFINED AF_SANITIZER) apply_sanitizers(bank_gui_qml_tests ${AF_SANITIZER}) endif() @@ -396,7 +395,7 @@ if(MORPH_BUILD_TESTS) # set for ctest (examples/TESTING.md). # # It does not cover the *discovery* run, and that gap cost this - # repository a permanently red CI leg (morph#690). PROPERTIES are + # repository a permanently red CI leg. PROPERTIES are # set on the tests Catch2 registers; the run that finds out what # those tests are happens first, and under DISCOVERY_MODE PRE_TEST # it happens inside ctest rather than inside the build. Catch2's diff --git a/examples/bank/README.md b/examples/bank/README.md index 3f6ae2d66..b1744381c 100644 --- a/examples/bank/README.md +++ b/examples/bank/README.md @@ -43,9 +43,8 @@ rather than using `morph::qt::forms::FormsControllerCore`), no `examples/common/gui` presenter architecture, and no [`TESTING.md`](../TESTING.md) dual-deployment-mode rig — its remote coverage is `SimulatedRemoteBackend` in `test_remote.cpp` plus the scenario corpus below. -Bringing those conventions into line is -[morph#87](https://github.com/LASTRADA-Software/morph/issues/87), which remains -open; the numbering question that issue also raises is the part that is settled. +Bringing those conventions into line is open work; the numbering question is +the part that is settled. ## Architecture: two type layers diff --git a/examples/bank/gui/CMakeLists.txt b/examples/bank/gui/CMakeLists.txt index c057ba7df..25b0a74c7 100644 --- a/examples/bank/gui/CMakeLists.txt +++ b/examples/bank/gui/CMakeLists.txt @@ -41,8 +41,9 @@ apply_bigobj(bank_gui_lib) # transitively and they are not -Werror clean. # # apply_sanitizers() is not the same question and does apply — see bank_lib's -# own block in ../CMakeLists.txt (morph#679). This is the library holding -# controllers/Format.hpp, where morph#663's undefined conversion was. +# own block in ../CMakeLists.txt. This is the library holding +# controllers/Format.hpp, whose double -> int64_t conversion is the kind of +# undefined behaviour only a sanitizer leg reports. if(DEFINED AF_SANITIZER) apply_sanitizers(bank_gui_lib ${AF_SANITIZER}) endif() diff --git a/examples/bank/gui/controllers/Format.hpp b/examples/bank/gui/controllers/Format.hpp index d23bca03d..ee78a27a4 100644 --- a/examples/bank/gui/controllers/Format.hpp +++ b/examples/bank/gui/controllers/Format.hpp @@ -88,7 +88,7 @@ inline constexpr double kMinorUnitsBound = 0x1p63; /// on a value whose result is outside `long long` raises a domain error and /// returns an unspecified value, exactly as converting one directly was UB /// ([conv.fpint]) before it, and `QString::toDouble` happily accepts `1e30` -/// from a QML text field with no validator (morph#663). The check is on the +/// from a QML text field with no validator. The check is on the /// *scaled* value rather than on @p text's value, because only the scaled /// value is what gets rounded -- `double` arithmetic itself cannot trap here, /// so computing it first costs nothing and removes the need to reason about @@ -96,7 +96,7 @@ inline constexpr double kMinorUnitsBound = 0x1p63; /// /// Rounding is to nearest, halves away from zero. It is `std::llround` rather /// than a `+ 0.5` and a truncation, which is not the same function: the two -/// disagree on the double immediately below one half (morph#678). +/// disagree on the double immediately below one half. /// /// @param text the user-entered amount, in major units /// @param decimals the number of minor-unit digits of the target currency @@ -116,7 +116,7 @@ inline std::optional parseMinor(const QString& text, int decimals // because `nan < 0.0` is false -- is rejected rather than let through. // // The guard is still what makes the line below defined, and it still runs - // first (morph#663). It bounds the *unrounded* value, which is the + // first. It bounds the *unrounded* value, which is the // stronger of the two: every `double` strictly below 2^63 is at most // 2^63-1024, so its rounding is inside `std::int64_t` with room to spare, // and the bound stays the one form that is exact. @@ -127,7 +127,7 @@ inline std::optional parseMinor(const QString& text, int decimals // on the double immediately below one half. 0.49999999999999994 + 0.5 is // exactly 1.0 in IEEE-754 -- the sum is not representable and rounds up -- // so truncating it charged a whole minor unit for an amount below half of - // one (morph#678). `llround` rounds to nearest with halves away from zero, + // one. `llround` rounds to nearest with halves away from zero, // which is what the `+ 0.5` was reaching for. return static_cast(std::llround(scaled)); } diff --git a/examples/bank/gui/qml/MoveMoneyPage.qml b/examples/bank/gui/qml/MoveMoneyPage.qml index 0a86b7d1a..403cacd44 100644 --- a/examples/bank/gui/qml/MoveMoneyPage.qml +++ b/examples/bank/gui/qml/MoveMoneyPage.qml @@ -35,7 +35,7 @@ ColumnLayout { // which is what refresh() does after each deposit, // withdrawal and transfer -- while txns keeps the account // the user chose, so the next deposit went somewhere the - // screen no longer named (morph#296). + // screen no longer named. // // refresh() emits accountsChanged before selectedChanged, // so this re-runs against the list that is already in place. diff --git a/examples/bank/include/bank/offline/lightweight_offline_queue.hpp b/examples/bank/include/bank/offline/lightweight_offline_queue.hpp index 2b64c8d4e..e66dc1746 100644 --- a/examples/bank/include/bank/offline/lightweight_offline_queue.hpp +++ b/examples/bank/include/bank/offline/lightweight_offline_queue.hpp @@ -19,7 +19,7 @@ /// morph owns the interface; the store is the application's. Nothing here is /// visible to `include/morph/` — `grep -rn Lightweight include/morph/` returns /// nothing, and that is the invariant this file exists to demonstrate rather -/// than to weaken (morph#549). The framework's own durable queues +/// than to weaken. The framework's own durable queues /// (`FileOfflineQueue`, `SqliteOfflineQueue`) are unchanged and unaffected: an /// application that already has an ORM connection open does not need a second /// persistence mechanism shipped by the framework, it needs the framework to diff --git a/examples/bank/tests/bank_test_support.hpp b/examples/bank/tests/bank_test_support.hpp index 20bd016b3..19c170704 100644 --- a/examples/bank/tests/bank_test_support.hpp +++ b/examples/bank/tests/bank_test_support.hpp @@ -25,7 +25,7 @@ namespace bank::testing { /// @brief The ODBC connection string every test in this process shares. /// /// A path of this process's own, so two ctest cases running concurrently never -/// name the same SQLite file (morph#682). +/// name the same SQLite file. /// /// @return The connection string. [[nodiscard]] inline const std::string& connectionString() { return uniqueDatabaseConnection(); } @@ -39,7 +39,7 @@ namespace bank::testing { /// /// The file is private to the process rather than a fixed path shared by every /// bank test binary -- see unique_test_database.hpp for why, and for what a -/// fixed path cost under `ctest -j` (morph#682). +/// fixed path cost under `ctest -j`. inline void ensureDatabase() { static const bool once = [] { bank::db::setup(connectionString()); @@ -94,7 +94,7 @@ T await(morph::async::Completion completion, morph::exec::MainThreadExecutor& return std::move(*value); } -// -- Why the two durations below are two types (morph#735) ------------------- +// -- Why the two durations below are two types ------------------- // // This `waitUntil` used to take `(Pred, milliseconds budget, milliseconds step, // MainThreadExecutor&)`: two adjacent, same-type parameters that every caller @@ -113,17 +113,17 @@ T await(morph::async::Completion completion, morph::exec::MainThreadExecutor& // `static_assert` block below pins every route back to the hazard. // // The same two types, with the same names and the same explicit constructors, -// are what `tests/test_support.hpp`'s framework `waitUntil` grew in morph#721, +// are what `tests/test_support.hpp`'s framework `waitUntil` takes, // and what `examples/kanban/tests/test_kanban_stress.cpp` carries -- the same // shape in three places rather than three shapes. They are redeclared here // because bank deliberately links neither `morph_ladder_testkit` nor the // framework's private test headers (see `examples/bank/CMakeLists.txt`'s own // note on why bank is not a ladder rung). // -// A `NOLINT` was not an option: it would remove the *warning* and leave the -// hazard (morph#404), and morph#715 measured the other near miss -- widening -// one parameter's type to silence `bugprone-easily-swappable-parameters` while -// the transposition still compiles. +// A `NOLINT` is not an option: it would remove the *warning* and leave the +// hazard. Nor is widening one parameter's type to silence +// `bugprone-easily-swappable-parameters`, which leaves the transposition +// compiling. /// @brief `waitUntil`'s overall polling budget: the longest it may wait before /// giving up and returning `false`. @@ -189,8 +189,8 @@ using ExampleWaitPred = bool (*)(); /// @brief A stand-in executor reference type for the assertions below. using ExampleGui = morph::exec::MainThreadExecutor&; -// The acceptance test for morph#735, in the header that owns the hazard, so it -// runs in every bank test translation unit that includes it. +// The acceptance test for that compile error, in the header that owns the +// hazard, so it runs in every bank test translation unit that includes it. // // What must keep working -- both live call sites pass all four arguments: static_assert(WaitUntilCallableWith); diff --git a/examples/bank/tests/gui/test_bank_gui_format.cpp b/examples/bank/tests/gui/test_bank_gui_format.cpp index a04518e75..3a3d98777 100644 --- a/examples/bank/tests/gui/test_bank_gui_format.cpp +++ b/examples/bank/tests/gui/test_bank_gui_format.cpp @@ -6,9 +6,9 @@ // // Why this file exists at all: `gui/controllers/Format.hpp` is a header under // `examples/`, and the root `.clang-tidy`'s `HeaderFilterRegex` discarded -// every finding in every such header (morph#664), so no analyser had ever +// every finding in every such header, so no analyser had ever // reported on it. What it contained was an unbounded `double` → `std::int64_t` -// conversion (morph#663): `QString::toDouble` accepts `1e30`, `inf` and `nan` +// conversion: `QString::toDouble` accepts `1e30`, `inf` and `nan` // from a QML field that carries no validator, and converting any of those is // undefined behaviour, not a large number. // @@ -55,12 +55,12 @@ TEST_CASE("parseMinor rejects text that is not a non-negative amount", "[bank][g CHECK_FALSE(parseMinor(QStringLiteral("-1.00")).has_value()); } -// The morph#663 regression. Each of these returned `-9223372036854775808` -// before the bound existed — via undefined behaviour, and via an abort under -// UBSan — and every call site then fed that through `.value_or(0)` into a +// The int64 bound. Without it each of these returns +// `-9223372036854775808` — via undefined behaviour, and via an abort under +// UBSan — and every call site then feeds that through `.value_or(0)` into a // balance. TEST_CASE("parseMinor rejects amounts that do not fit in int64 minor units", "[bank][gui][format]") { - // The value the issue reproduced with: 1e30 major units scale to 1e32. + // The easiest value to reach it with: 1e30 major units scale to 1e32. CHECK_FALSE(parseMinor(QStringLiteral("1e30")).has_value()); CHECK_FALSE(parseMinor(QStringLiteral("1e300")).has_value()); @@ -94,8 +94,8 @@ TEST_CASE("parseMinor's ceiling is the int64 range, not an arbitrary cap", "[ban CHECK_FALSE(parseMinor(QStringLiteral("920000000000000000")).has_value()); } -// The morph#678 regression. `static_cast(x + 0.5)` is not -// "round to nearest": for the double immediately below 0.5, adding 0.5 rounds +// Why `std::llround` and not `x + 0.5`. `static_cast(x + 0.5)` is +// not "round to nearest": for the double immediately below 0.5, adding 0.5 rounds // *up* to exactly 1.0 in IEEE-754, and the truncating cast then yields 1 for a // value that is below half a minor unit. // @@ -110,15 +110,15 @@ TEST_CASE("parseMinor rounds a value just below half a minor unit down", "[bank] // x = 0.49999999999999994449 // x < 0.5 = true // x + 0.5 = 1 - // (int64)(x + 0.5) = 1 <- what this function returned - // std::llround(x) = 0 + // (int64)(x + 0.5) = 1 <- the wrong answer + // std::llround(x) = 0 <- what this function returns // // Not a constructed bit pattern: a decimal string short enough to type // into the amount field, through `QString::toDouble`. CHECK(parseMinor(QStringLiteral("0.004999999999999999")) == 0); CHECK(parseMinor(QStringLiteral("0.0049999999999999994")) == 0); - // morph#663's bound still comes first. Rounding a value outside the int64 + // The int64 bound still comes first. Rounding a value outside the int64 // range is no better defined than casting one, so an amount that cannot // fit has to be rejected before it is rounded, not after. CHECK_FALSE(parseMinor(QStringLiteral("1e30")).has_value()); diff --git a/examples/bank/tests/gui/test_bank_gui_qml_behaviour.cpp b/examples/bank/tests/gui/test_bank_gui_qml_behaviour.cpp index c6f9445ec..9a733e7b1 100644 --- a/examples/bank/tests/gui/test_bank_gui_qml_behaviour.cpp +++ b/examples/bank/tests/gui/test_bank_gui_qml_behaviour.cpp @@ -1,22 +1,22 @@ // SPDX-License-Identifier: Apache-2.0 // // The bank GUI's behaviour that lives *in* the QML, over a live engine and the -// real controllers (morph#296). +// real controllers. // // Why an engine rather than a controller-only test, stated up front because it // is the whole reason this binary exists: the controllers are entirely // self-consistent when driven from C++. Write `selectedAccount`, refresh, // deposit, and the money lands in the account that was written, every time. -// The defect morph#296 named was not in the controller at all -- it was that -// `MoveMoneyPage.qml` wrote `selectedAccount` and never read it back, so -// nothing restored the `ComboBox`'s `currentIndex` after -// `TransactionController::refresh()` republished `accounts`. A `ComboBox` +// The defect this holds off is not in the controller at all: it is a +// `MoveMoneyPage.qml` that writes `selectedAccount` and never reads it back, so +// nothing restores the `ComboBox`'s `currentIndex` after +// `TransactionController::refresh()` republishes `accounts`. A `ComboBox` // resets `currentIndex` to 0 whenever its `model` is replaced, and `refresh()` // is exactly what `deposit()`/`withdraw()`/`transfer()` call on success. The -// picker therefore snapped back to the first account while the controller kept -// the account the user chose, and the next deposit went somewhere the screen -// did not say. Only a live engine over the shipped `.qml` can observe that, so -// only a live engine can hold the regression. +// picker then snaps back to the first account while the controller keeps the +// account the user chose, and the next deposit goes somewhere the screen does +// not say. Only a live engine over the shipped `.qml` can observe that, so only +// a live engine can hold the property. // // The second case covers the other half of the same issue: `txns.posted` and // `payees.paid` were emitted and dropped. They are now the success half of @@ -130,9 +130,9 @@ namespace { // two accounts, stand up a QQmlEngine, load MoveMoneyPage.qml and drive the // picker onto the savings account before it can assert anything of its own, // and that prologue is eight assertions -- 32 -- on its own. Hoisting it into a -// helper moves the score into the helper instead of removing it. And morph#296's -// defect *is* the sequence -- pick, deposit, still picked, deposit again, the -// money followed the label -- which is the thing a split would scatter. +// helper moves the score into the helper instead of removing it. And the defect +// *is* the sequence -- pick, deposit, still picked, deposit again, the money +// followed the label -- which is the thing a split would scatter. // // Two per-case directives rather than one entry in // examples/bank/tests/.clang-tidy, which would subtract the check from every @@ -217,9 +217,9 @@ TEST_CASE("MoveMoneyPage's picker keeps naming the account the next deposit will REQUIRE(pumpUntil([&txns] { return txns.accounts().size() == 2; })); REQUIRE(balanceOf(savings) == 5000); - // The picker must still name the savings account. Before morph#296's fix - // it named the checking account here: replacing a ComboBox's `model` resets - // its `currentIndex` to 0, and nothing read `selectedAccount` back. + // The picker must still name the savings account. Without the read-back it + // names the checking account here: replacing a ComboBox's `model` resets + // its `currentIndex` to 0. CHECK(picker->property("currentValue").toLongLong() == txns.selectedAccount()); // ── and it is not cosmetic: the money follows the label ─────────────── diff --git a/examples/bank/tests/gui/test_bank_qml_surface.cpp b/examples/bank/tests/gui/test_bank_qml_surface.cpp index b0a33e466..aab753400 100644 --- a/examples/bank/tests/gui/test_bank_qml_surface.cpp +++ b/examples/bank/tests/gui/test_bank_qml_surface.cpp @@ -62,7 +62,7 @@ TEST_CASE("Every bank controller exposes exactly the surface gui/qml binds, and // metaobjects and text — but `BankClient`'s constructor runs the schema // migrations, so it needs a database like any other bank test does. A file // private to this process, so no other ctest case — in this binary or - // another — can be unlinking it while this one has it open (morph#682). + // another — can be unlinking it while this one has it open. bankgui::BankClient client{bank::testing::uniqueDatabaseConnection()}; // const: `QmlSurfaceAudit::bind` takes `const QObject&`, and nothing here @@ -107,15 +107,14 @@ TEST_CASE("Every bank controller exposes exactly the surface gui/qml binds, and } // ── No backlog ──────────────────────────────────────────────────────── - // The five `refresh` invokables above are the whole exemption list. The - // first run of this audit also reported four members no file under gui/qml/ - // bound — `txns.selectedAccount` with its `selectedChanged`, `txns.posted` - // and `payees.paid` — recorded here as morph#296 and since resolved by - // binding all four rather than deleting any: MoveMoneyPage.qml now reads - // the picker's selection back out of the controller (which is what stops a - // deposit landing in an account the screen no longer names) and Main.qml - // handles both success signals as a toast, opposite the `onError` toast - // that was already there. Nothing else is exempt, in either direction. + // The five `refresh` invokables above are the whole exemption list, and + // nothing else is exempt in either direction. Four members that an audit + // like this one naturally reports -- `txns.selectedAccount` with its + // `selectedChanged`, `txns.posted` and `payees.paid` -- are bound rather + // than exempted: MoveMoneyPage.qml reads the picker's selection back out of + // the controller, which is what stops a deposit landing in an account the + // screen no longer names, and Main.qml handles both success signals as a + // toast, opposite the `onError` toast beside it. const QStringList findings = audit.run(); INFO(findings.join(QStringLiteral("\n")).toStdString()); diff --git a/examples/bank/tests/test_lightweight_offline_queue.cpp b/examples/bank/tests/test_lightweight_offline_queue.cpp index 44761561f..2dc128483 100644 --- a/examples/bank/tests/test_lightweight_offline_queue.cpp +++ b/examples/bank/tests/test_lightweight_offline_queue.cpp @@ -2,7 +2,7 @@ // // Runs morph's own IOfflineQueue conformance suite against a queue morph does // not ship: bank::offline::LightweightOfflineQueue, whose store is the -// Lightweight ORM (morph#549). +// Lightweight ORM. // // The suite is included from morph's tests/ directory **unedited**. That is the // point of the exercise, not an incidental detail: a suite adjusted until a new @@ -32,7 +32,7 @@ using bank::offline::OfflineQueueRecord; /// /// The conformance suite's `make` factory must hand back an *empty* store on /// every call, and this process has exactly one database (its own private file -/// -- see unique_test_database.hpp, morph#682). Truncating the one table is +/// -- see unique_test_database.hpp). Truncating the one table is /// what "a fresh store" means here. void truncateQueueTable() { bank::testing::ensureDatabase(); diff --git a/examples/bank/tests/test_pool_scope.cpp b/examples/bank/tests/test_pool_scope.cpp index b9d06d128..ccfc3a94e 100644 --- a/examples/bank/tests/test_pool_scope.cpp +++ b/examples/bank/tests/test_pool_scope.cpp @@ -17,12 +17,12 @@ /// @file /// Why `examples/common/db/pool_transaction_audit.hpp` is **not** installed in -/// bank (morph#752). +/// bank. /// -/// morph#740's `PoolTransactionAudit` detects a pooled `Lightweight::DataMapper` -/// handed on with `SQL_ATTR_AUTOCOMMIT` still `OFF`. It is installed from all -/// seven ladder rungs and from `testkit/DbFixture`; morph#752 proposed -/// extending it to bank, on the premise that "bank's models acquire from +/// `PoolTransactionAudit` detects a pooled `Lightweight::DataMapper` handed on +/// with `SQL_ATTR_AUTOCOMMIT` still `OFF`. It is installed from all seven ladder +/// rungs and from `testkit/DbFixture`, so extending it to bank looks like an +/// oversight closed -- on the premise that "bank's models acquire from /// `Lightweight::GlobalDataMapperPool()` like every rung does". /// /// That premise is false, and this file is the measurement that says so. @@ -43,8 +43,8 @@ /// and requires it to move. Only then is the second half's zero evidence. /// /// It is also the tripwire the audit would have been: if bank ever moves its -/// models onto `GlobalDataMapperPool()`, this case fails and morph#752 becomes -/// live again. +/// models onto `GlobalDataMapperPool()`, this case fails and installing the +/// audit here becomes worth doing. namespace { diff --git a/examples/bank/tests/unique_test_database.hpp b/examples/bank/tests/unique_test_database.hpp index 147bd29c2..3982873bc 100644 --- a/examples/bank/tests/unique_test_database.hpp +++ b/examples/bank/tests/unique_test_database.hpp @@ -18,7 +18,7 @@ /// process deleted and re-migrated that file on the way in — which is correct /// under a serial `ctest` and nothing else. Under `ctest -j`, 21 processes /// unlinked and re-created one file concurrently and 21 of 21 cases failed with -/// `HY000 (10) - [SQLite]disk I/O error (10)` (morph#682). +/// `HY000 (10) - [SQLite]disk I/O error (10)`. /// /// The remedy the ladder took for the same hazard is `RESOURCE_LOCK` (see /// `cmake/morph_add_rung.cmake`), which serialises the cases instead. Bank does From 8d8ab1ec55fae7078c976453b8c38ad79b4e8927 Mon Sep 17 00:00:00 2001 From: Yaraslau Tamashevich Date: Thu, 24 Sep 2026 03:05:40 +0200 Subject: [PATCH 04/10] comments(examples/ledger): state the constraint, drop the citation The largest rung tree: README, SYNC-BENCHMARK.md, the DTO and model headers, book_access.hpp's ownership/scope argument, and eleven test suites. Kept whole, because each is a live constraint rather than a story: the rollback-journal vs. WAL contention contrast on `ReadTxnGuard` (with the `DataMapperPool::Return` consequence and the 60s `busy_timeout` that follows from it), `requireCategoryInBook`'s argument for a write-side-only guard and why the refusal is `NotFound` and not `ValidationError`, the report-job scope comparison's worked failure (`Done` is terminal, so the correct body can never be computed afterwards), and SYNC-BENCHMARK.md's +50.00 balance that the per-currency zero-sum invariant cannot see. Rewritten rather than deleted: everywhere a paragraph argued from when a thing changed. "Until morph#373 this was accepted" becomes what accepting it would cost; "morph#183 replaced the hand-written traits" becomes what a hand-written trait has to do that the macro does not. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW --- examples/ledger/CMakeLists.txt | 2 +- examples/ledger/README.md | 79 +++++++++---------- examples/ledger/SYNC-BENCHMARK.md | 22 +++--- examples/ledger/gui/main.cpp | 2 +- examples/ledger/gui/qml/LedgerView.qml | 10 +-- examples/ledger/gui_lib/ledger_presenter.hpp | 2 +- examples/ledger/gui_lib/ledger_qml_bridge.cpp | 2 +- examples/ledger/gui_lib/ledger_qml_bridge.hpp | 4 +- examples/ledger/include/ledger/app/app.hpp | 12 +-- .../include/ledger/auth/ledger_authorizer.hpp | 2 +- .../ledger/include/ledger/core/time_util.hpp | 2 +- examples/ledger/include/ledger/core/types.hpp | 2 +- examples/ledger/include/ledger/core/units.hpp | 2 +- .../ledger/include/ledger/db/book_access.hpp | 20 ++--- .../include/ledger/db/ledger_entity.hpp | 4 +- .../ledger/include/ledger/dto/account_dto.hpp | 3 +- .../ledger/include/ledger/dto/budget_dto.hpp | 2 +- .../ledger/include/ledger/dto/report_dto.hpp | 4 +- .../include/ledger/dto/transaction_dto.hpp | 17 ++-- .../include/ledger/models/budget_model.hpp | 14 ++-- .../include/ledger/models/ledger_model.hpp | 33 ++++---- .../include/ledger/models/rule_model.hpp | 12 +-- examples/ledger/src/db/schema.cpp | 6 +- examples/ledger/src/models/budget_model.cpp | 24 +++--- examples/ledger/src/models/ledger_model.cpp | 54 ++++++------- examples/ledger/tests/test_app.cpp | 19 +++-- .../ledger/tests/test_ledger_authorizer.cpp | 19 +++-- .../tests/test_ledger_book_ownership.cpp | 18 ++--- .../tests/test_ledger_category_scope.cpp | 31 ++++---- examples/ledger/tests/test_ledger_import.cpp | 8 +- .../tests/test_ledger_list_transactions.cpp | 10 +-- examples/ledger/tests/test_ledger_model.cpp | 12 +-- .../ledger/tests/test_ledger_model_keys.cpp | 21 +++-- .../ledger/tests/test_ledger_presenter.cpp | 2 +- .../ledger/tests/test_ledger_qml_surface.cpp | 30 +++---- examples/ledger/tests/test_ledger_reports.cpp | 55 +++++++------ .../ledger/tests/test_report_presenter.cpp | 4 +- 37 files changed, 278 insertions(+), 287 deletions(-) diff --git a/examples/ledger/CMakeLists.txt b/examples/ledger/CMakeLists.txt index 9c3d98efd..2d39481f7 100644 --- a/examples/ledger/CMakeLists.txt +++ b/examples/ledger/CMakeLists.txt @@ -12,7 +12,7 @@ morph_add_rung(NAME ledger) # morph_add_rung() only globs src/models/*.cpp, src/db/*.cpp and # src/app/*.cpp into ladder_ledger_lib (cmake/morph_add_rung.cmake) -- it # does not know about this rung's src/dto/ (Login's principal-charset -# validation, morph#242) or src/auth/ (none needed: LedgerAuthorizer is +# validation) or src/auth/ (none needed: LedgerAuthorizer is # header-only, unlike PollsAuthorizer's split .cpp -- see # ledger/auth/ledger_authorizer.hpp's own doc comment). Without an explicit # target_sources() call the rung fails to link with undefined diff --git a/examples/ledger/README.md b/examples/ledger/README.md index 30f65077e..1f50db2df 100644 --- a/examples/ledger/README.md +++ b/examples/ledger/README.md @@ -52,10 +52,10 @@ here that carries no `ledgerId` — it is what mints one — so it is dispatched keyless, the same shape `polls::PollModel` gives `CreatePoll`. Any authenticated principal may call it; a caller with no token is refused `unauthorized` by `LedgerAuthorizer` before the model is entered, and an empty -principal by the model itself. Added by morph#361: until then a `ledgers` row -was created by no registered action at all, and a freshly started -`ladder_ledger_server` against a new database served a book nobody could open -(`OpenAccount` refused with `OpenAccount: no such ledger`). +principal by the model itself. Without it no registered action would create a +`ledgers` row at all, and a freshly started `ladder_ledger_server` against a new +database would serve a book nobody could open (`OpenAccount` refusing with +`OpenAccount: no such ledger`). **Whose book it is.** `CreateLedger` records its caller as the book's owner, and every action that reaches a book compares that owner against the @@ -85,7 +85,7 @@ rows, and there is no principal to attribute those rows to; `NULL` therefore means "created before ownership existed", the same reading `params_json` already has on a report job. Nothing writes a new one — `CreateLedger` always stamps its caller — but the scenario corpus's fixture books are seeded by raw -`INSERT` and are unowned for this reason. Added by morph#382. +`INSERT` and are unowned for this reason. **Which book it is.** Owning both books is not the same as their being one book. An account and a category are joined only when they belong to the *same* @@ -100,15 +100,14 @@ rather than filtering on a scope argument; the guard is `ledger::db::requireCategoryInBook` and it lives beside the ownership one in `ledger/db/book_access.hpp`. -Until morph#373 this was accepted, and what made it survivable was an -invariant stated nowhere: `GetBudgetReport` filters legs by the budget's own -ledger's journals, so a foreign account's legs never reached the sum. A -guarantee resting on every future report kind keeping a filter nobody wrote -down is the shape morph#384 rejected, so the link is refused at the write -instead. +Accepting the cross-book pair would be survivable only through an invariant +stated nowhere: `GetBudgetReport` filters legs by the budget's own ledger's +journals, so a foreign account's legs never reach the sum. A guarantee resting +on every future report kind keeping a filter nobody wrote down is half a scheme, +so the link is refused at the write instead. -**The guard is write-side only.** A row written before morph#373 may still -hold a cross-book link, and nothing rewrites or refuses it: no migration +**The guard is write-side only.** A row written before the guard applied may +still hold a cross-book link, and nothing rewrites or refuses it: no migration touches the `accounts` or `budgets` rows, and every read still answers. Two reasons. A cleanup would have to *choose* which side to break — null the account's `category_id`, or move the row into the other book — and both @@ -125,15 +124,16 @@ membership table: there is no way to share a book with a second principal, and no `kanban`-style `project_roles` to promote anyone through. A book is its creator's, and everyone else is refused. -**One action a client cannot drive to a result (morph#362).** Every action +**One action a client cannot drive to a result.** Every action below is registered on the wire, and one of them answers only with a refusal no matter what a client sends. It is recorded in `scripts/scenario/coverage_allowlist.json` so the workflow-coverage gate does not chase it. -This used to be two. `UndoTransaction` was the other, and morph#428 closed it -by adding the missing read rather than by writing the gap down — see -"`ListTransactions` is how an entry gets named" below. +`UndoTransaction` is *not* one of them, and the reason is worth knowing before +adding a second entry to that allowlist: the read that makes it drivable was +added rather than the gap written down — see "`ListTransactions` is how an entry +gets named" below. - **`RunReportJob` is the report runner's, not a client's.** `LedgerModel::execute(const RunReportJob&)` refuses every principal but @@ -146,19 +146,19 @@ by adding the missing read rather than by writing the gap down — see runner's own tick land or does not. It cannot advance its own job, and asking to is a refusal, not a slow success. -**`ListTransactions` is how an entry gets named (morph#428).** -`UndoTransaction { ledgerId, journalId }` reverses one journal entry, and until -morph#428 `JournalId` appeared in exactly one wire DTO in this rung — that -action's own input. Nothing returned one: `StoreTransaction` and -`UndoTransaction` answer `GetLedgerResult` (accounts and balances), `GetLedger` -the same, `ImportLedgerChunk` answers counts, and there was no `GetJournal` and -no listing. So the only outcome a client could reach was the not-found refusal -`UndoTransaction: no such journal`, against an id it guessed, while the -in-process tests got the id from the database — and the desktop client shipped -an Undo button whose only input was a number no screen ever displayed. - -That question is settled, the same way morph#361/#384 settled the equivalent -one for `CreateLedger`: by adding the action. `ListTransactions { ledgerId, +**`ListTransactions` is how an entry gets named.** +`UndoTransaction { ledgerId, journalId }` reverses one journal entry, and +`ListTransactions` is the only thing in the rung that hands a `JournalId` back. +Nothing else returns one: `StoreTransaction` and `UndoTransaction` answer +`GetLedgerResult` (accounts and balances), `GetLedger` the same, +`ImportLedgerChunk` answers counts, and there is no `GetJournal`. Without the +listing the only outcome a client could reach would be the not-found refusal +`UndoTransaction: no such journal` against a guessed id — in-process tests can +read the id out of the database, but a desktop client's Undo button would have +no screen to read it from. + +The answer is the same one `CreateLedger` gets for the equivalent question: +add the action. `ListTransactions { ledgerId, month }` answers `{ entries: [{ id, description, date, legs }] }` for one `"YYYY-MM"` month of one book, oldest first, gated by `db::requireOwnedBook` like every other book-reaching read. `LedgerView.qml`'s Undo control now takes @@ -188,8 +188,7 @@ Build order (status as of rung 5's implementation, see instead -- two clients both reversing the same transaction offline -- found a real bug (both `UndoTransaction`s applied, doubling the reversal), fixed by `causal_parent_id` naming what a compensating entry reverses and a second - reversal being rejected with `AlreadyReversed`. morph#144 tracked both - halves and is closed. + reversal being rejected with `AlreadyReversed`. 1. Accounts + `StoreTransaction { description, date, legs[] }` — one @@ -232,7 +231,7 @@ Build order (status as of rung 5's implementation, see SQLite WAL read transaction; the byte-identical DoD is only meaningful against that snapshot. - **Who runs the job (morph#160).** `SubmitReport` writes a `Pending` row + **Who runs the job.** `SubmitReport` writes a `Pending` row and returns; it schedules nothing and starts no thread. `ledger::app::App` — this rung's App layer — sweeps for `Pending` rows on a timer and dispatches `RunReportJob` back at `LedgerModel`, where the aggregation @@ -253,7 +252,7 @@ Build order (status as of rung 5's implementation, see design's queued lambda died with its process. **The App owns a `RemoteServer`, fronted by `ladder_ledger_server` - (morph#242).** `RemoteServer` clears the session principal for any + .** `RemoteServer` clears the session principal for any authorizer that does not authenticate (`docs/spec/security.md`), so a real login story needed a real, verifying authorizer: `LedgerAuthorizer` (`ledger/auth/ledger_authorizer.hpp`) plus `AuthModel`/`Login` @@ -388,12 +387,12 @@ data; the submit→poll job idiom. unit at `dp=0` and the type system carries it natively. Named test: a JPY leg stores and displays as a true integer, with no `x-rules` gate required. -- **Locale entry** — *fixed, morph#574*: in de-DE the group separator is "." - and the shipped normalizer stripped it anywhere, so typing `1.5` submitted - **15**, a silent 10× money error. `normalizeLocaleNumber` and its QML mirror - now validate group placement (one to three digits before, exactly three - after, never past the decimal separator) and report a malformed entry - instead; `"1.050,25"` still normalises. Remaining from this item: +- **Locale entry** — *fixed*: in de-DE the group separator is "." and a + normalizer that strips it anywhere turns a typed `1.5` into **15**, a silent + 10× money error. `normalizeLocaleNumber` and its QML mirror validate group + placement (one to three digits before, exactly three after, never past the + decimal separator) and report a malformed entry instead; `"1.050,25"` still + normalises. Remaining from this item: result *display* in the shipped forms renderer goes through `double` division — balances beyond 2^53 drift on readback while the payload is exact. This rung's own views do not: every money label binds diff --git a/examples/ledger/SYNC-BENCHMARK.md b/examples/ledger/SYNC-BENCHMARK.md index 8aaab6aed..0c9f08cf5 100644 --- a/examples/ledger/SYNC-BENCHMARK.md +++ b/examples/ledger/SYNC-BENCHMARK.md @@ -69,14 +69,13 @@ entry names the entry it reverses (`transaction_journals.causal_parent_id`), so this is a query against the audit trail, not mutable state on the original, which stays immutable. -This one was worth running rather than asserting. Before the check existed, -the second reversal applied: a reversed -50.00/+50.00 shop left Checking at -**+50.00**, money the user never had. The ledger's headline per-currency -zero-sum invariant does not catch it — a compensating entry is itself -zero-sum, so the total stays 0.00 while the individual balances go wrong. -Only the per-account balances show it. See -`tests/test_sync_benchmark.cpp`'s "Scenario A, in the form this rung can -express"; morph#144 carries the finding. +This one was worth running rather than asserting. Without the check the second +reversal applies: a reversed -50.00/+50.00 shop leaves Checking at **+50.00**, +money the user never had. The ledger's headline per-currency zero-sum invariant +does not catch it — a compensating entry is itself zero-sum, so the total stays +0.00 while the individual balances go wrong. Only the per-account balances show +it. See `tests/test_sync_benchmark.cpp`'s "Scenario A, in the form this rung can +express". ## Scenario B — a stale base version is rejected, never merged @@ -102,9 +101,10 @@ this document argues against. The mechanism is `UpdateRule::expectedVersion` -- optional, so an unconditional update stays unconditional, and engaged when a client wants its -edit refused rather than applied blind. Before morph#144 the `version` column -existed and incremented on every write but nothing ever compared it: this -section described intended behaviour with no code behind it. +edit refused rather than applied blind. The `version` column on its own is not +the mechanism: it increments on every write whether or not anything compares +it, so a section like this one describes intended behaviour until something +does. ## Clock skew: client timestamps are claimed, never authoritative diff --git a/examples/ledger/gui/main.cpp b/examples/ledger/gui/main.cpp index 664f03a5d..7dac7ae7a 100644 --- a/examples/ledger/gui/main.cpp +++ b/examples/ledger/gui/main.cpp @@ -2,7 +2,7 @@ // /// @file /// ledger's desktop client shell: one `AppContext` (deployment mode chosen -/// from argv), a login step (morph#242) before any domain bridge is usable, +/// from argv), a login step before any domain bridge is usable, /// the four QML bridges Tasks 18-21 define built inside `ctx.onReady()`, and /// a `QQmlApplicationEngine` loading this rung's own QML module. /// diff --git a/examples/ledger/gui/qml/LedgerView.qml b/examples/ledger/gui/qml/LedgerView.qml index f6a47c51b..2713edfcc 100644 --- a/examples/ledger/gui/qml/LedgerView.qml +++ b/examples/ledger/gui/qml/LedgerView.qml @@ -94,11 +94,11 @@ ColumnLayout { } // ── Entries, and the Undo control they feed ────────────────────────── - // The journal id is shown rather than typed. Until morph#428 this was a - // bare "Journal id to undo" TextField, and no screen in this rung -- and - // no reply on the wire -- ever displayed a journal id, so the only way to - // fill it in was to guess. `listTransactions` is where the ids come from - // now; `undoTransaction` is handed one of them straight back. + // The journal id is shown rather than typed. A bare "Journal id to undo" + // TextField would be unfillable except by guessing: nothing else in this + // rung, and no reply on the wire, displays a journal id. + // `listTransactions` is where the ids come from; `undoTransaction` is + // handed one of them straight back. RowLayout { Layout.fillWidth: true Label { text: qsTr("Entries") ; font.bold: true } diff --git a/examples/ledger/gui_lib/ledger_presenter.hpp b/examples/ledger/gui_lib/ledger_presenter.hpp index 5c3936bff..a1fea958c 100644 --- a/examples/ledger/gui_lib/ledger_presenter.hpp +++ b/examples/ledger/gui_lib/ledger_presenter.hpp @@ -95,7 +95,7 @@ class LedgerPresenter : public ::morph::ladder::gui::Presenter { /// `transactionsListed` on success, `failed` on error. /// /// The read that gives `undoTransaction` below a source for its - /// `journalId` (morph#428): before it, the Undo control this + /// `journalId`: before it, the Undo control this /// rung's `LedgerView.qml` ships had to be typed a number no /// screen ever displayed. /// @param ledgerId The ledger whose entries to list. diff --git a/examples/ledger/gui_lib/ledger_qml_bridge.cpp b/examples/ledger/gui_lib/ledger_qml_bridge.cpp index b9f054d3f..c1f4eb268 100644 --- a/examples/ledger/gui_lib/ledger_qml_bridge.cpp +++ b/examples/ledger/gui_lib/ledger_qml_bridge.cpp @@ -93,7 +93,7 @@ using ::morph::ladder::gui::idNumber; /// /// `id` is the whole point of the listing: it is the number /// `undoTransaction` asks for, published so a view can hand it back -/// rather than asking a user to guess it (morph#428). +/// rather than asking a user to guess it. /// /// No amounts. A `TransactionLeg` carries no currency of its own -- /// the currency belongs to the account the leg names (design spec §2) diff --git a/examples/ledger/gui_lib/ledger_qml_bridge.hpp b/examples/ledger/gui_lib/ledger_qml_bridge.hpp index 8f46b7356..81789b7ac 100644 --- a/examples/ledger/gui_lib/ledger_qml_bridge.hpp +++ b/examples/ledger/gui_lib/ledger_qml_bridge.hpp @@ -43,8 +43,8 @@ class LedgerQmlBridge : public QObject { /// @brief The last listed month's journal entries, each a map of /// `id`/`description`/`dateText` -- the `id` being the number - /// `undoTransaction` asks for, which until morph#428 no screen in - /// this rung ever displayed. + /// `undoTransaction` asks for, and the only place in this rung + /// that displays it. Q_PROPERTY(QVariantList entries READ entries NOTIFY entriesChanged) /// @brief `true` while any dispatch is in flight, for a busy indicator. diff --git a/examples/ledger/include/ledger/app/app.hpp b/examples/ledger/include/ledger/app/app.hpp index 67bbedb37..dcf6c9ae7 100644 --- a/examples/ledger/include/ledger/app/app.hpp +++ b/examples/ledger/include/ledger/app/app.hpp @@ -16,16 +16,16 @@ /// @file /// `ledger::app::App` -- rung 5's server-side bootstrap: the model worker -/// pool, the `RemoteServer` a standalone `ladder_ledger_server` (morph#242) +/// pool, the `RemoteServer` a standalone `ladder_ledger_server` /// stands a transport in front of, the process-global `TokenIssuer` and /// `LedgerAuthorizer` that give this rung a real auth story, and the one /// background job this rung has. /// -/// The report runner exists because rung 5 had nowhere to put a background -/// job and put it in a model instead: `LedgerModel` owned a -/// `ThreadPoolExecutor` and `SubmitReport` posted the aggregation to it, -/// making the one ladder model that included `` -/// (morph#160). The layering that resolves it is the same one +/// The report runner exists because a rung with nowhere else to put a +/// background job puts it in a model instead: `LedgerModel` would own a +/// `ThreadPoolExecutor`, `SubmitReport` would post the aggregation to it, and +/// this would be the one ladder model including ``. +/// The layering that avoids that is the same one /// `bookmarks::app::App` already demonstrates: the App owns the worker pool /// and decides *when* work runs; the model still owns *what* the work /// computes, and is re-entered as an ordinary client dispatch diff --git a/examples/ledger/include/ledger/auth/ledger_authorizer.hpp b/examples/ledger/include/ledger/auth/ledger_authorizer.hpp index 15e9a7577..3ac2f2555 100644 --- a/examples/ledger/include/ledger/auth/ledger_authorizer.hpp +++ b/examples/ledger/include/ledger/auth/ledger_authorizer.hpp @@ -34,7 +34,7 @@ /// `Context::principal` with the verified identity before the model runs, /// each mutating model action refusing an empty principal /// (`EmptyPrincipalError`, design spec §11) or, for `RunReportJob`, refusing -/// any principal but `kReportRunnerPrincipal` -- and, since morph#382, +/// any principal but `kReportRunnerPrincipal` -- and /// **per-book ownership**: `CreateLedger` records its caller on the `ledgers` /// row and every action reaching a book compares that owner against /// `Context::principal`, reads included. That last one is deliberately *not* diff --git a/examples/ledger/include/ledger/core/time_util.hpp b/examples/ledger/include/ledger/core/time_util.hpp index 2d28927fa..2fec5c82e 100644 --- a/examples/ledger/include/ledger/core/time_util.hpp +++ b/examples/ledger/include/ledger/core/time_util.hpp @@ -22,7 +22,7 @@ /// because they now have more than one. `isValidYearMonth` was in /// `ledger/dto/budget_dto.hpp` and `monthRangeMs` in an anonymous namespace in /// `src/models/budget_model.cpp`; `ListTransactions` takes the same -/// `"YYYY-MM"` bound as `GetBudgetReport` (morph#428) and must parse it the +/// `"YYYY-MM"` bound as `GetBudgetReport` and must parse it the /// same way, and a second copy of a date parser is a second thing to get /// wrong. diff --git a/examples/ledger/include/ledger/core/types.hpp b/examples/ledger/include/ledger/core/types.hpp index c06c511db..54c4b663a 100644 --- a/examples/ledger/include/ledger/core/types.hpp +++ b/examples/ledger/include/ledger/core/types.hpp @@ -57,7 +57,7 @@ enum class ReportStatus : std::uint8_t { Pending, Done, Failed }; /// whether a rung renders any form -- fails to compile: glaze emits a /// six-way wildcard type for a metaless enum, and the shipped Qt/QML /// `DynamicForm` draws that as a checkbox reporting the form `ready` -/// for a value nobody chose (morph#392). Same shape as +/// for a value nobody chose. Same shape as /// `kanban::Role`/`kanban::RuleMutationType` /// (examples/kanban/include/kanban/core/types.hpp), the rungs that /// established this convention. diff --git a/examples/ledger/include/ledger/core/units.hpp b/examples/ledger/include/ledger/core/units.hpp index 159802168..54c568cc8 100644 --- a/examples/ledger/include/ledger/core/units.hpp +++ b/examples/ledger/include/ledger/core/units.hpp @@ -77,7 +77,7 @@ using UnitTraits = morph::units::UnitTraits; /// (`account_dto.hpp`'s `Currency currency;`, `budget_dto.hpp`'s /// `Currency currency;`) -- so, like every other closed-set field this /// rung declares, it needs a `glz::meta`/`glz::enumerate` for -/// `morph::forms::schemaJson()` to describe it (morph#392). Spelled +/// `morph::forms::schemaJson()` to describe it. Spelled /// the same as `currencyToCode`'s own 3-letter codes, so the wire /// encoding and the DB column encoding agree. template <> diff --git a/examples/ledger/include/ledger/db/book_access.hpp b/examples/ledger/include/ledger/db/book_access.hpp index dcf71fedc..3bfa696c7 100644 --- a/examples/ledger/include/ledger/db/book_access.hpp +++ b/examples/ledger/include/ledger/db/book_access.hpp @@ -12,9 +12,9 @@ #include "ledger/db/ledger_entity.hpp" /// @file -/// The single home for this rung's "whose book is this?" rule (morph#382), and +/// The single home for this rung's "whose book is this?" rule, and /// for the "*which* book is this?" rule that sits beside it -/// (`requireCategoryInBook`, morph#373). +/// (`requireCategoryInBook`). /// /// **Where the rule lives, and why not at the authorizer.** /// `examples/IMPLEMENTATION.md` rule 4 puts ownership authorization *through @@ -137,7 +137,7 @@ inline void requireOwnedParentBook(Lightweight::DataMapper& mapper, std::uint64_ } /// @brief Refuses a category that belongs to a book other than @p bookLedgerId -/// -- *which* book, where everything above answers *whose* (morph#373). +/// -- *which* book, where everything above answers *whose*. /// /// Three actions join a category to something else by id alone: /// `SetCategory` and `LinkAccountToCategory` join it to an account, @@ -151,21 +151,21 @@ inline void requireOwnedParentBook(Lightweight::DataMapper& mapper, std::uint64_ /// `examples/IMPLEMENTATION.md` rule 1 is what makes that a defect rather than /// a documented liberty: a model re-checks its own preconditions, and "an /// account and a category are the same book's" is a precondition this rung -/// documented and did not check. What made the mis-scoped row survivable was -/// an invariant nothing states -- `GetBudgetReport` filters legs by the -/// budget's own ledger's journals, so a foreign account's legs never reach the -/// sum -- and a guarantee that rests on every future report keeping a filter -/// nobody wrote down is the half-a-scheme shape morph#384 rejected. +/// documents. Leaving it unchecked would be survivable only through an +/// invariant nothing states -- `GetBudgetReport` filters legs by the budget's +/// own ledger's journals, so a foreign account's legs never reach the sum -- +/// and a guarantee resting on every future report keeping a filter nobody +/// wrote down is half a scheme, not a rule. /// /// Neither `SetCategory` nor `LinkAccountToCategory` carries a `ledgerId`, so -/// this cannot be a `Where` folded into the lookup the way morph#380's +/// this cannot be a `Where` folded into the lookup the way /// `accountInLedger` scopes a leg's account against the ledger its action /// names. It is a comparison of the two loaded rows' own `ledger` values /// instead -- which is also why the refusal is raised after the not-found and /// ownership ones, leaving their wording and ordering untouched. /// /// `NotFound`, and a message of its own: `accountInLedger`'s exact idiom for -/// the identical question about an account, for the reason morph#380 gave -- +/// the identical question about an account, and for the same reason -- /// a client that cannot tell "that id names nothing" from "that id is in your /// other book" cannot tell a dead id from a mis-scoped one. Not /// `ValidationError`: the request is well-formed, and every other "wrong book" diff --git a/examples/ledger/include/ledger/db/ledger_entity.hpp b/examples/ledger/include/ledger/db/ledger_entity.hpp index 6b4ed9d71..b6a75b651 100644 --- a/examples/ledger/include/ledger/db/ledger_entity.hpp +++ b/examples/ledger/include/ledger/db/ledger_entity.hpp @@ -24,7 +24,7 @@ struct LedgerRecord { static constexpr std::string_view TableName = "ledgers"; Light::Field id; // 0 Light::Field, Light::SqlRealName{"name"}> name; // 1 - // The principal that created this book (morph#382). Every action reaching + // The principal that created this book. Every action reaching // this book compares it against `session::current()->principal`; see // `ledger/db/book_access.hpp`, which is the single home for that rule. // @@ -197,7 +197,7 @@ struct ReportJobRecord { // `SubmitReport::params` verbatim, so the job row records what it was // asked to compute and not merely that it was asked. Load-bearing since // the aggregation moved out of SubmitReport's own call frame and into - // RunReportJob (morph#160): the runner that eventually settles this job + // RunReportJob: the runner that eventually settles this job // may be in a different process from the one that accepted it, and has // nothing but this row to reconstruct the request from. // diff --git a/examples/ledger/include/ledger/dto/account_dto.hpp b/examples/ledger/include/ledger/dto/account_dto.hpp index 29199dd00..156bb0319 100644 --- a/examples/ledger/include/ledger/dto/account_dto.hpp +++ b/examples/ledger/include/ledger/dto/account_dto.hpp @@ -24,7 +24,7 @@ namespace ledger { inline constexpr std::size_t kMaxLedgerNameBytes = 128; /// @brief Creates a book -- the root entity every other ledger action keys -/// off (morph#361). +/// off. /// /// Lives here, beside `OpenAccount`/`GetLedger`, because those are the other /// two actions whose subject is the book itself rather than what is posted @@ -44,7 +44,6 @@ inline constexpr std::size_t kMaxLedgerNameBytes = 128; /// promise than `kanban::CreateProject`'s, which makes its caller the first /// `Manager` of a real role table: this rung has no roles and no way to share /// a book, so the owner is simply the one principal that may use it. -/// morph#382. struct CreateLedger { std::string name; diff --git a/examples/ledger/include/ledger/dto/budget_dto.hpp b/examples/ledger/include/ledger/dto/budget_dto.hpp index 93b5919ce..664a1f326 100644 --- a/examples/ledger/include/ledger/dto/budget_dto.hpp +++ b/examples/ledger/include/ledger/dto/budget_dto.hpp @@ -6,7 +6,7 @@ #include // `detail::isValidYearMonth` moved to ledger/core/time_util.hpp when -// `ListTransactions` gained the same "YYYY-MM" bound (morph#428); it is still +// `ListTransactions` gained the same "YYYY-MM" bound; it is still // `ledger::detail::isValidYearMonth`, still called from the two validate()s // below, and now has one definition rather than two. #include "ledger/core/time_util.hpp" diff --git a/examples/ledger/include/ledger/dto/report_dto.hpp b/examples/ledger/include/ledger/dto/report_dto.hpp index 7135fe959..2963fd0a5 100644 --- a/examples/ledger/include/ledger/dto/report_dto.hpp +++ b/examples/ledger/include/ledger/dto/report_dto.hpp @@ -20,7 +20,7 @@ namespace ledger { /// carrying `kind` and `params` verbatim. Nothing is scheduled and no /// thread is started -- the row *is* the queue, and /// `ledger::app::App`'s report runner is what drains it, by -/// dispatching `RunReportJob` back at this model (morph#160). A job +/// dispatching `RunReportJob` back at this model. A job /// submitted while no runner is up therefore stays `Pending` and is /// picked up by the first pass of the next one, which is what makes a /// report survive a server restart instead of dying with the process @@ -67,7 +67,7 @@ struct MonthlyStatementParams { /// not as a lambda posted to an executor the model owns, because the /// aggregation is business logic and business logic lives in a model /// (`examples/IMPLEMENTATION.md` rule 1) -- while deciding *when* it -/// runs is orchestration, which does not (morph#160). Re-entering the +/// runs is orchestration, which does not. Re-entering the /// model as an ordinary dispatch is the same shape /// `bookmarks::RecordMetadata` has for that rung's metadata worker. /// diff --git a/examples/ledger/include/ledger/dto/transaction_dto.hpp b/examples/ledger/include/ledger/dto/transaction_dto.hpp index 6452abfb1..2897d7b12 100644 --- a/examples/ledger/include/ledger/dto/transaction_dto.hpp +++ b/examples/ledger/include/ledger/dto/transaction_dto.hpp @@ -120,22 +120,21 @@ struct UndoTransaction { /// that takes a `JournalId`. /// /// This action exists because nothing else in the rung's wire surface -/// ever hands a `journalId` back (morph#428). `StoreTransaction` and +/// hands a `journalId` back. `StoreTransaction` and /// `UndoTransaction` both answer with `GetLedgerResult` -- the /// accounts and their balances -- `GetLedger` the same, and -/// `ImportLedgerChunk` with counts. `JournalId` appeared in exactly -/// one DTO field in the whole rung, and that field was -/// `UndoTransaction`'s own *input*. The rung's own tests reached -/// around that by querying the row through a `DataMapper`, which is -/// precisely what a WebSocket client and the QML bridge do not have, -/// so the shipped Undo control had no source for the one number it -/// asks for. +/// `ImportLedgerChunk` with counts. Without this action the only +/// `JournalId` in any DTO field is `UndoTransaction`'s own *input*: +/// a test can reach around that by querying the row through a +/// `DataMapper`, but a WebSocket client and the QML bridge have no +/// `DataMapper`, so the shipped Undo control would have no source for +/// the one number it asks for. /// /// Month-bounded, in the exact `"YYYY-MM"` shape /// `GetBudgetReport::month` uses and validated by the same /// `detail::isValidYearMonth`, so the result cannot grow without /// bound as a book ages. That bound is the whole bounding mechanism -- -/// deliberately no cursor and no page size (morph#428's own scope): +/// deliberately no cursor and no page size: /// one month of one book is a quantity a client can hold, and a /// pagination protocol is a wire contract worth designing on its own /// evidence rather than inventing here. diff --git a/examples/ledger/include/ledger/models/budget_model.hpp b/examples/ledger/include/ledger/models/budget_model.hpp index b98be4d35..2c6bcd57e 100644 --- a/examples/ledger/include/ledger/models/budget_model.hpp +++ b/examples/ledger/include/ledger/models/budget_model.hpp @@ -95,13 +95,13 @@ BRIDGE_REGISTER_ACTION(ledger::BudgetModel, ledger::GetBudgetReport, "GetBudgetR // (model_key.hpp's `KeyTypeOf`) -- and each line below records only that the // action carries the key. // -// Until morph#183 these were hand-written specialisations, because -// `morph::model::ModelKey` admitted only `std::integral`/`std::string` and no -// `LEDGER_DEFINE_STRONG_ID` type qualified. morph#163 widened it, and with the -// hand-written bodies went their `*action.ledgerId`/`*action.budgetId` -- -// `operator*` on a possibly-disengaged `std::optional`, undefined behaviour -// for an action carrying an empty id. `morph::model::keyToString` refuses an -// empty strong id instead of unwrapping it. +// Macros rather than hand-written `ActionKeyTraits` specialisations, which is +// possible because `morph::model::ModelKey` admits a `LEDGER_DEFINE_STRONG_ID` +// type and not only `std::integral`/`std::string`. A hand-written body has to +// unwrap the id itself -- `*action.ledgerId`/`*action.budgetId`, `operator*` on +// a possibly-disengaged `std::optional`, undefined behaviour for an action +// carrying an empty id -- where `morph::model::keyToString` refuses an empty +// strong id instead. // // LinkAccountToCategory carries two ids (accountId, categoryId) and no // single natural "the" key. `ActionKeyTraits`'s primary template diff --git a/examples/ledger/include/ledger/models/ledger_model.hpp b/examples/ledger/include/ledger/models/ledger_model.hpp index a053213a2..b22b46af9 100644 --- a/examples/ledger/include/ledger/models/ledger_model.hpp +++ b/examples/ledger/include/ledger/models/ledger_model.hpp @@ -84,7 +84,7 @@ class LedgerModel { using PrimaryKey = std::int64_t; /// @brief Creates a book and returns its id -- the bootstrap every other - /// action on this model depends on (morph#361). + /// action on this model depends on. /// /// The one action here that carries no `ledgerId`, because it is /// the action that produces one. It is therefore dispatched @@ -139,7 +139,7 @@ class LedgerModel { /// @brief Lists the journal entries `action.ledgerId` recorded during /// `action.month`, oldest first, each carrying the `JournalId` a - /// client needs to name it (morph#428). + /// client needs to name it. /// /// The read that makes `UndoTransaction` -- and the Undo control /// `gui/qml/LedgerView.qml` ships -- drivable at all: before this, @@ -148,7 +148,7 @@ class LedgerModel { /// only ever pass an id it had guessed. /// /// Gated by `db::requireOwnedBook` exactly as `GetLedger`, - /// `GetBudgetReport` and `GetReportStatus` are (morph#382): a + /// `GetBudgetReport` and `GetReportStatus` are: a /// listing of a book's entries is precisely the kind of read that /// gate exists for. Carries no `EmptyPrincipalError` gate, for the /// same reason `execute(GetLedger)` does not -- an empty principal @@ -250,7 +250,7 @@ class LedgerModel { /// and this model owns no executor. Draining that row is /// `ledger::app::App`'s report runner's job, which dispatches /// `RunReportJob` back at this model as an ordinary client action - /// (morph#160) -- the same shape bookmarks' metadata worker uses, + /// -- the same shape bookmarks' metadata worker uses, /// and the reason this header no longer includes /// `` at all. /// @param action The ledger id, report kind, and JSON-encoded params. @@ -465,19 +465,18 @@ BRIDGE_REGISTER_ACTION(ledger::LedgerModel, ledger::GetLedger, "GetLedger", ::mo // Each of these lines records only that the action carries the key, and // routes the value through `morph::model::keyToString`. // -// Until morph#183 these were seven hand-written `ActionKeyTraits` -// specialisations plus a `ModelKeyTraits`, because -// `morph::model::ModelKey` admitted only `std::integral`/`std::string` and -// `ledger::LedgerId` (like every `LEDGER_DEFINE_STRONG_ID` type, types.hpp) -// wraps a `std::optional`. morph#163 widened the concept to -// admit a strong id, so the macros work on these fields now -- and each -// hand-written body's `*action.ledgerId` is gone with them. That dereference -// was `operator*` on a possibly-disengaged `std::optional`: undefined -// behaviour for an action carrying an empty id, which handed back whatever -// the union held and routed the caller to an arbitrary instance. -// `keyToString` refuses an empty strong id instead, and -// `BridgeHandler::execute`'s `catch (...)` around key extraction turns the -// refusal into a rejected `Completion`. +// The macros work on these fields -- rather than seven hand-written +// `ActionKeyTraits` specialisations plus a `ModelKeyTraits` -- +// because `morph::model::ModelKey` admits a strong id and not only +// `std::integral`/`std::string`, and `ledger::LedgerId` (like every +// `LEDGER_DEFINE_STRONG_ID` type, types.hpp) wraps a +// `std::optional`. A hand-written body has to write +// `*action.ledgerId`: `operator*` on a possibly-disengaged `std::optional`, +// undefined behaviour for an action carrying an empty id, handing back whatever +// the union held and routing the caller to an arbitrary instance. `keyToString` +// refuses an empty strong id instead, and `BridgeHandler::execute`'s +// `catch (...)` around key extraction turns the refusal into a rejected +// `Completion`. BRIDGE_KEY_FROM(ledger::OpenAccount, &ledger::OpenAccount::ledgerId); BRIDGE_KEY_FROM(ledger::GetLedger, &ledger::GetLedger::ledgerId); diff --git a/examples/ledger/include/ledger/models/rule_model.hpp b/examples/ledger/include/ledger/models/rule_model.hpp index dac27dea7..799cfa3f1 100644 --- a/examples/ledger/include/ledger/models/rule_model.hpp +++ b/examples/ledger/include/ledger/models/rule_model.hpp @@ -75,12 +75,12 @@ BRIDGE_REGISTER_ACTION(ledger::RuleModel, ledger::UpdateRule, "UpdateRule") // `BRIDGE_MODEL_KEY`'s deduced type is the right one and nothing overrides it: // `PrimaryKeyOf` is `ledger::LedgerId` itself, the strong id // examples/IMPLEMENTATION.md rule 3 asks for, rather than the unwrapped -// `std::int64_t` this model declared by hand while `morph::model::ModelKey` -// still admitted only raw scalars (morph#163 widened it, morph#183 migrated -// this). The `*action.ledgerId` the hand-written body performed -- `operator*` -// on a possibly-disengaged `std::optional`, undefined behaviour for an empty -// id -- is gone with it: `morph::model::keyToString` refuses an empty strong -// id, which `BridgeHandler::execute` turns into a rejected `Completion`. +// `std::int64_t` a hand-written specialisation would have to declare. +// `morph::model::ModelKey` admits a strong id, so no hand-written body has to +// perform `*action.ledgerId` -- `operator*` on a possibly-disengaged +// `std::optional`, undefined behaviour for an empty id. Instead +// `morph::model::keyToString` refuses an empty strong id, which +// `BridgeHandler::execute` turns into a rejected `Completion`. BRIDGE_MODEL_KEY(ledger::RuleModel, ledger::CreateRule, &ledger::CreateRule::ledgerId); // UpdateRule carries a ruleId, not a ledgerId, so it cannot share diff --git a/examples/ledger/src/db/schema.cpp b/examples/ledger/src/db/schema.cpp index 5b2b66c2c..11e722f82 100644 --- a/examples/ledger/src/db/schema.cpp +++ b/examples/ledger/src/db/schema.cpp @@ -8,7 +8,7 @@ namespace ledger::db { void configure(const std::string& connectionString) { - // morph#740: nothing in Lightweight stops a pooled DataMapper from being + // Nothing in Lightweight stops a pooled DataMapper from being // returned with a transaction still open on it -- `DataMapperPool::Return` // does no transaction cleanup, and the cost lands on the next, unrelated // borrower as a 60s stall and a `database is locked` it did not cause. @@ -200,7 +200,7 @@ LIGHTWEIGHT_SQL_MIGRATION(20260819000013, "Create ledger_applied_ops table") { LIGHTWEIGHT_SQL_MIGRATION(20260819000014, "Store SubmitReport params with the job row") { // The job row has to be self-describing now that the aggregation no - // longer runs inside SubmitReport's own call frame (morph#160): the + // longer runs inside SubmitReport's own call frame: the // params used to be decoded on the caller's thread and captured into the // posted lambda, so nothing needed to persist them. With the run moved // to ledger::app::App's runner -- possibly in a different process, and @@ -216,7 +216,7 @@ LIGHTWEIGHT_SQL_MIGRATION(20260819000014, "Store SubmitReport params with the jo } LIGHTWEIGHT_SQL_MIGRATION(20260819000015, "Record which principal owns each book") { - // Per-book ownership (morph#382). Until this column existed the rung had + // Per-book ownership. Until this column existed the rung had // no notion of whose book a `ledgers` row was: the signed-token check and // the per-action empty-principal gate both held, and neither says *whose* // book this is, so any authenticated principal could read, write and post diff --git a/examples/ledger/src/models/budget_model.cpp b/examples/ledger/src/models/budget_model.cpp index 024f72836..1fd2b272a 100644 --- a/examples/ledger/src/models/budget_model.cpp +++ b/examples/ledger/src/models/budget_model.cpp @@ -12,7 +12,7 @@ #include "clock.hpp" #include "ledger/core/errors.hpp" #include "ledger/core/money.hpp" -#include "ledger/core/time_util.hpp" // monthRangeMs -- shared with LedgerModel's ListTransactions (morph#428) +#include "ledger/core/time_util.hpp" // monthRangeMs -- shared with LedgerModel's ListTransactions #include "ledger/db/book_access.hpp" #include "ledger/db/ledger_entity.hpp" @@ -121,15 +121,15 @@ AccountId BudgetModel::execute(const LinkAccountToCategory& action) { // Both sides, because this action names two rows and nothing else // constrains them to the same book. What this refuses is a link across // an *ownership* boundary -- someone else's account under your - // category, or yours under someone else's (morph#382). + // category, or yours under someone else's. db::requireOwnedParentBook(mapper, accountRows.front().ledger.Value(), ctx->principal, "LinkAccountToCategory"); db::requireOwnedParentBook(mapper, categoryRows.front().ledger.Value(), ctx->principal, "LinkAccountToCategory"); - // And then *which* book, which ownership alone never answered: two - // books one principal owns, and two unowned ones, could be - // cross-linked until morph#373 closed it. Last of the three refusals, - // so the not-found and ownership ones keep their wording and ordering. + // And then *which* book, which ownership alone does not answer: two + // books one principal owns, and two unowned ones, would otherwise be + // cross-linkable. Last of the three refusals, so the not-found and + // ownership ones keep their wording and ordering. db::requireCategoryInBook(categoryRows.front().ledger.Value(), accountRows.front().ledger.Value(), "LinkAccountToCategory"); accountRows.front().category = categoryRows.front(); @@ -163,7 +163,7 @@ BudgetId BudgetModel::execute(const CreateBudget& action) { throw NotFound{"CreateBudget: no such ledger or category"}; } // The named book, and the category's own book -- a budget joins the - // two, so owning one of them is not enough (morph#382). The existence + // two, so owning one of them is not enough. The existence // check above keeps its combined message; ownership is a separate // refusal. if (!db::bookIsReachableBy(ledgerRows.front(), ctx->principal)) { @@ -171,7 +171,7 @@ BudgetId BudgetModel::execute(const CreateBudget& action) { } db::requireOwnedParentBook(mapper, categoryRows.front().ledger.Value(), ctx->principal, "CreateBudget"); // ...and the category's book must *be* the named book, not merely be - // owned by the same principal (morph#373). This is the site that + // owned by the same principal. This is the site that // decides which `categoryId` `execute(GetBudgetReport)` below fans its // account lookup out over, so a budget filed under book one naming a // book-two category is the one cross-book row with report @@ -247,7 +247,7 @@ GetBudgetReportResult BudgetModel::execute(const GetBudgetReport& action) { } // A pure read, gated the same way `LedgerModel::execute(GetLedger)` is: // spent-so-far against a budget is the book's activity, and it was - // readable by every authenticated principal (morph#382). + // readable by every authenticated principal. db::requireOwnedParentBook(mapper, budgetRows.front().ledger.Value(), db::currentPrincipal(), "GetBudgetReport"); auto limitRows = mapper.Query() .Where(::Lightweight::FieldNameOf<&db::BudgetLimitRecord::budget>, "=", *action.budgetId) @@ -276,9 +276,9 @@ GetBudgetReportResult BudgetModel::execute(const GetBudgetReport& action) { const auto categoryId = budgetRows.front().category.Value(); const auto ledgerId = budgetRows.front().ledger.Value(); // Scoped to the budget's own ledger as well as its category, because - // morph#373's guard is write-side only: a row written before it existed - // may still hold a cross-book link, and this is what makes such a row - // provably inert rather than inert-by-argument. Without it, step 1 + // `requireCategoryInBook` is a write-side guard only: a row written before + // it applied may still hold a cross-book link, and this is what makes such + // a row provably inert rather than inert-by-argument. Without it, step 1 // collects the foreign account and the whole exclusion rests on step 2's // journal filter -- an invariant no future report kind is bound by. auto categoryAccountRows = mapper.Query() diff --git a/examples/ledger/src/models/ledger_model.cpp b/examples/ledger/src/models/ledger_model.cpp index 2edd94a1e..8cf303c4f 100644 --- a/examples/ledger/src/models/ledger_model.cpp +++ b/examples/ledger/src/models/ledger_model.cpp @@ -44,7 +44,7 @@ static_assert(decltype(db::LedgerRecord::owner)::ValueType::value_type{}.capacit "ledger::db::LedgerRecord::owner must be exactly as wide as the longest principal Login will mint a " "token for (ledger::auth::kMaxPrincipalBytes) -- Light::SqlFixedString truncates rather than throwing, " "so a narrower column would store a shortened owner that the very principal who created the book can " - "never match, locking them out of it (morph#382)."); + "never match, locking them out of it."); namespace { @@ -318,7 +318,7 @@ void checkZeroSumByCurrency(const std::vector& legAmounts /// DEFERRED` on construction, `COMMIT` on destruction). /// /// @par Why this is not called a WAL snapshot guard -/// It was, and the name was wrong. **Nothing in this rung, its +/// Because that name would be wrong. **Nothing in this rung, its /// server, or `scripts/scenario/` ever issues `PRAGMA /// journal_mode`**, and Lightweight's `SqlConnection::PostConnect()` /// declines to set WAL on purpose (`src/Lightweight/SqlConnection.cpp` @@ -336,17 +336,17 @@ void checkZeroSumByCurrency(const std::vector& legAmounts /// file for as long as it is held**. /// /// This rung's report pass holds one across a whole aggregation -/// (`computeReportJson`) on a background pool thread, so the old name -/// described the opposite of the contention this guard actually -/// causes -- a misdirection for anyone debugging `database is locked` -/// here (morph#739). The consistent-view property the call site wants -/// is still real: in rollback-journal mode it is delivered by the -/// write barrier rather than by a snapshot. Turning WAL on to make -/// the original name true is a separate, larger decision (WAL is +/// (`computeReportJson`) on a background pool thread, so a snapshot +/// name would describe the opposite of the contention this guard +/// actually causes -- a misdirection for anyone debugging +/// `database is locked` here. The consistent-view property the call +/// site wants is still real: in rollback-journal mode it is delivered +/// by the write barrier rather than by a snapshot. Turning WAL on to +/// make the snapshot name true is a separate, larger decision (WAL is /// per-database-file and persists in the file header, so it reaches /// every other connection and the scenario runner's /// fresh-database-per-run assumption) and is deliberately **not** -/// taken here; morph#739 records the argument. +/// taken here. /// /// Load-bearing because `Lightweight::DataMapperPool::Return` /// performs no transaction cleanup on a returned connection (it @@ -563,7 +563,7 @@ void finishReportJob(Lightweight::DataMapper& mapper, std::int64_t jobId, Report /// action named, and since every read *is* scoped, the named book's /// reply does not mention the account it moved and the other book shows /// a balance change with no journal of its own to explain it. The two -/// books disagree and neither report says so (morph#367). +/// books disagree and neither report says so. /// /// The ledger is compared after the lookup rather than folded into it /// as a second `Where`, so "no such account" and "that account is in @@ -677,7 +677,7 @@ CreateLedgerResult LedgerModel::execute(const CreateLedger& action) { Lightweight::DataMapper mapper; db::LedgerRecord ledgerRow; ledgerRow.name = Light::SqlAnsiString<128>{action.name}; - // The caller owns what it creates (morph#382). This is the only place + // The caller owns what it creates. This is the only place // an owner is ever written: every other action reads it, and a book // whose owner is NULL is one written before this column existed. ledgerRow.owner = Light::SqlAnsiString<64>{ctx->principal}; @@ -702,7 +702,7 @@ AccountInfo LedgerModel::execute(const OpenAccount& action) { } Lightweight::DataMapper mapper; // The ledger row must already exist -- `execute(const CreateLedger&)` - // above is what creates one (morph#361). Load it by primary key + // above is what creates one. Load it by primary key // rather than fabricating a stub LedgerRecord, since // BelongsTo assignment needs the real persisted parent (per // polls::db::OptionRecord's own `opt.poll = poll;` usage, where `poll` @@ -747,10 +747,10 @@ GetLedgerResult LedgerModel::execute(const GetLedger& action) { throw ValidationError{"GetLedger: ledgerId is required"}; } Lightweight::DataMapper mapper; - // A read is where the gap was widest: this action had no principal check - // of any kind, so a second authenticated client could ask for -- and get - // -- every account and balance in a book it had nothing to do with - // (morph#382). It carries no EmptyPrincipalError gate even now, because + // A read is where the gap would be widest: without a principal check of + // any kind, a second authenticated client could ask for -- and get -- + // every account and balance in a book it has nothing to do with. + // It carries no EmptyPrincipalError gate, because // it does not need one: an empty principal never matches a recorded // owner, so it is refused here and admitted only for an unowned book, // which is exactly what it could always reach. @@ -770,7 +770,7 @@ ListTransactionsResult LedgerModel::execute(const ListTransactions& action) { Lightweight::DataMapper mapper; // Same gate, same reason, as execute(GetLedger) directly above: a listing // of a book's entries is a read of the book, so it goes through - // db::requireOwnedBook (morph#382) and needs no EmptyPrincipalError gate + // db::requireOwnedBook and needs no EmptyPrincipalError gate // of its own -- an empty principal never matches a recorded owner. static_cast(db::requireOwnedBook(mapper, action.ledgerId, db::currentPrincipal(), "ListTransactions")); @@ -857,7 +857,7 @@ GetLedgerResult LedgerModel::execute(const StoreTransaction& action) { // `GetLedgerResult` -- every account and balance in the book -- and the // account lookups are a "does account N belong to book B" oracle, so a // gate placed after either would hand a non-owner exactly what the - // `GetLedger` gate exists to withhold (morph#382). + // `GetLedger` gate exists to withhold. const auto ledgerRow = db::requireOwnedBook(mapper, action.ledgerId, ctx->principal, "StoreTransaction"); // Task 11b, design spec §1 (kanban's execute(MoveTaskPosition) pattern, @@ -1100,7 +1100,7 @@ GetLedgerResult LedgerModel::execute(const UndoTransaction& action) { } // The book gate runs after the journal is resolved, not before it, so // the two "no such journal" refusals this action already had keep - // their exact wording and order (morph#382). The journal names its own + // their exact wording and order. The journal names its own // ledger and that has just been verified against the action's, so // gating on it is gating on the book the action really reaches. db::requireOwnedParentBook(mapper, originalJournalRow.ledger.Value(), ctx->principal, "UndoTransaction"); @@ -1424,7 +1424,7 @@ RunReportJobResult LedgerModel::execute(const RunReportJob& action) { // action checked only its own job row. Without it a job whose ledger // has since been deleted aggregates an empty account set, produces // `[]` and settles Done, so a caller cannot tell "no such ledger" - // from "a ledger with no activity" (morph#250). + // from "a ledger with no activity". // // Raised *inside* this try on purpose. Throwing out of the method // instead would leave the row Pending, and ledger::app::App re-sweeps @@ -1443,8 +1443,8 @@ RunReportJobResult LedgerModel::execute(const RunReportJob& action) { // own ledger on its row. Without this comparison // `RunReportJob{jobId: , ledgerId: }` settles // book two's job `Done` carrying book one's totals, terminally, and - // `GetReportStatus` then hands those back as book two's report - // (morph#371). Same shape as `accountInLedger`'s ledger comparison and + // `GetReportStatus` then hands those back as book two's report. Same + // shape as `accountInLedger`'s ledger comparison and // `execute(UndoTransaction)`'s journal check -- an id resolved without // the scope the action names. // @@ -1549,7 +1549,7 @@ GetReportStatusResult LedgerModel::execute(const GetReportStatus& action) { throw NotFound{"GetReportStatus: no such job"}; } const auto& row = jobRows.front(); - // A job id carries no ledgerId of its own (morph#371), so the book this + // A job id carries no ledgerId of its own, so the book this // read reaches is the one the job row names. Gated after the job lookup so // "no such job" keeps its wording, and like `execute(GetLedger)` this pure // read needs no separate empty-principal gate: an empty principal matches @@ -1594,7 +1594,7 @@ void LedgerModel::setCategoryImpl(Lightweight::DataMapper& mapper, const SetCate // Both call sites reach this: the public `execute(SetCategory)` overload, // where it is the only book gate the action gets, and the rule cascade // inside `execute(StoreTransaction)`, where the caller has already passed - // the same gate on the same book and this one passes too (morph#382). + // the same gate on the same book and this one passes too. // // Both *rows*, too, the way `BudgetModel::execute(LinkAccountToCategory)` // checks both of its: this action joins two rows nothing else constrains @@ -1605,14 +1605,14 @@ void LedgerModel::setCategoryImpl(Lightweight::DataMapper& mapper, const SetCate const auto principal = db::currentPrincipal(); db::requireOwnedParentBook(mapper, accountRows.front().ledger.Value(), principal, "SetCategory"); db::requireOwnedParentBook(mapper, categoryRows.front().ledger.Value(), principal, "SetCategory"); - // Then *which* book (morph#373): owning both is not the same as their + // Then *which* book: owning both is not the same as their // being one book, and until this check existed a caller could file its own // account in book two under its own category in book one. // // Unreachable from the rule cascade, which is the other caller: that path // looks its category up with a `Where` on the triggering action's own // `ledgerId`, and its account is a leg account, which `accountInLedger` - // has already constrained to that same ledger (morph#380). The two are + // has already constrained to that same ledger. The two are // therefore always one book there, and this refusal is a live gate only // for the client-facing `execute(SetCategory)` above. db::requireCategoryInBook(categoryRows.front().ledger.Value(), accountRows.front().ledger.Value(), "SetCategory"); diff --git a/examples/ledger/tests/test_app.cpp b/examples/ledger/tests/test_app.cpp index 59f175219..50efba2cc 100644 --- a/examples/ledger/tests/test_app.cpp +++ b/examples/ledger/tests/test_app.cpp @@ -94,7 +94,7 @@ constexpr std::string_view kTestTokenSecret = "ledger-app-test-secret"; } // namespace TEST_CASE("App::runPendingReportsOnce settles a job the model only wrote a row for", "[ledger][app]") { - // The whole of morph#160 in one case: the model accepts the submission + // The submit/run split in one case: the model accepts the submission // and does nothing else -- no executor, no thread -- and the App layer is // what turns the Pending row into a computed report, by dispatching // RunReportJob back at the model on its own strand. @@ -248,17 +248,16 @@ TEST_CASE("The App runs a job for every ledger, not only the first", "[ledger][a } TEST_CASE("A job whose ledger no longer exists settles Failed", "[ledger][app]") { - // morph#250, now closed. This test previously pinned the opposite: the - // aggregation found no accounts, produced `[]`, and the job settled Done, - // so a caller could not tell "no such ledger" from "a ledger with no - // activity" and the App's failure arm was unreachable this way. - // `RunReportJob` now checks its ledger row the way every sibling action - // does (OpenAccount, StoreTransaction, ImportLedgerChunk, SubmitReport, - // storeJournalImpl). + // Without a ledger check the aggregation finds no accounts, produces `[]`, + // and the job settles Done -- so a caller cannot tell "no such ledger" from + // "a ledger with no activity" and the App's failure arm is unreachable this + // way. `RunReportJob` therefore checks its ledger row the way every sibling + // action does (OpenAccount, StoreTransaction, ImportLedgerChunk, + // SubmitReport, storeJournalImpl). // // The check is raised inside the aggregation's own try block, so the job - // still settles *terminally*, which is the property the original test was - // written to guard. Throwing out of the action instead would leave the row + // still settles *terminally*, which is the property that has to hold + // alongside it. Throwing out of the action instead would leave the row // Pending, and `runPendingReportsOnce` re-sweeps every Pending row on // every pass -- the same doomed job would be re-dispatched forever. // diff --git a/examples/ledger/tests/test_ledger_authorizer.cpp b/examples/ledger/tests/test_ledger_authorizer.cpp index c497795d5..25dcbafe6 100644 --- a/examples/ledger/tests/test_ledger_authorizer.cpp +++ b/examples/ledger/tests/test_ledger_authorizer.cpp @@ -15,14 +15,13 @@ #include "testkit/db_fixture.hpp" #include "testkit/pump.hpp" -// morph#242: this rung had no authentication/authorization story, so every -// mutating action failed in the shipped desktop client, in both deployment -// modes -- Local (no login was ever installed) and Remote (RemoteServer -// clears an unverified principal, and this rung shipped no authorizer that -// could verify one, and no server binary). This file exercises the Remote- -// mode half end to end, the same shape `test_bookmarks_authorizer.cpp` uses -// for its own rung's identical fix: a tokenless client is refused, `Login` -// mints a real signed token, and that token unlocks the rest. +// Without an authentication/authorization story every mutating action fails in +// the shipped desktop client, in both deployment modes -- Local (no login +// installed) and Remote (RemoteServer clears an unverified principal, so an +// absent authorizer means nothing can verify one). This file exercises the +// Remote-mode half end to end, the same shape `test_bookmarks_authorizer.cpp` +// uses for its own rung: a tokenless client is refused, `Login` mints a real +// signed token, and that token unlocks the rest. using ledger::auth::isReservedPrincipal; using ledger::auth::isValidPrincipal; @@ -198,8 +197,8 @@ TEST_CASE("setTokenIssuer/tokenIssuer share one process-global slot", "[ledger][ } TEST_CASE("A tokenless client logs in over a real RemoteServer and its token unlocks the rest", "[ledger][auth]") { - // The end-to-end shape of morph#242's fix, at the wire level: this is the - // exact sequence a freshly launched Remote-mode desktop client performs. + // The whole login path at the wire level: this is the exact sequence a + // freshly launched Remote-mode desktop client performs. DbFixture fixture; const auto authorizer = std::make_shared(std::string{kSecret}, morph::session::hmacSha256); // RAII, not a trailing reset: a failing REQUIRE below throws, and a diff --git a/examples/ledger/tests/test_ledger_book_ownership.cpp b/examples/ledger/tests/test_ledger_book_ownership.cpp index 908bb7a67..c236f03a1 100644 --- a/examples/ledger/tests/test_ledger_book_ownership.cpp +++ b/examples/ledger/tests/test_ledger_book_ownership.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 // -// Per-book authorization (morph#382). +// Per-book authorization. // // Until this file existed, `ledger` had no per-book ownership of any kind: // any principal holding a valid token could read, write and post into any @@ -56,9 +56,9 @@ class ScopedPrincipal { morph::session::detail::ScopedContext _scope; }; -/// @brief A book with no recorded owner -- the shape every `ledgers` row had -/// before morph#382's migration, written the only way one can still be -/// written now that `CreateLedger` always stamps its caller. +/// @brief A book with no recorded owner -- written the only way one can be, +/// now that `CreateLedger` always stamps its caller: by inserting the +/// row directly, as the scenario corpus's fixtures do. [[nodiscard]] ledger::LedgerId unownedBook(Lightweight::DataMapper& mapper, const std::string& name) { ledger::db::LedgerRecord row; row.name = Light::SqlAnsiString<128>{name}; @@ -114,11 +114,11 @@ TEST_CASE("A second principal can neither read nor write a book it does not own" const ScopedPrincipal bob{"bob"}; - // The read half. `GetLedger` had no principal check at all, so this is - // where a second client learned the whole book. + // The read half. Without a principal check on `GetLedger` this is where a + // second client learns the whole book. CHECK_THROWS_AS(model.execute(ledger::GetLedger{.ledgerId = book}), ledger::Forbidden); - // The write half, in the two shapes the reproduction on morph#382 used. + // The write half, in the two shapes that reach a book by id alone. CHECK_THROWS_AS(model.execute(ledger::OpenAccount{.ledgerId = book, .name = "Bob's account in Alice's book", .kind = ledger::AccountKind::Liability, @@ -332,8 +332,8 @@ TEST_CASE("A book written before the owner column stays open to everyone", "[led // populate rows that already exist and there is no principal to attribute // them to, so a NULL owner means "created before ownership existed" -- the // same reading `params_json` already has on the report-job row -- and such - // a book behaves exactly as every book did before morph#382. The scenario - // corpus's fixture books are written this way. + // a book is open to every authenticated principal. The scenario corpus's + // fixture books are written this way. morph::ladder::testkit::DbFixture fixture; Lightweight::DataMapper mapper; const auto book = unownedBook(mapper, "Scenario book"); diff --git a/examples/ledger/tests/test_ledger_category_scope.cpp b/examples/ledger/tests/test_ledger_category_scope.cpp index 3a9a636ea..91457c511 100644 --- a/examples/ledger/tests/test_ledger_category_scope.cpp +++ b/examples/ledger/tests/test_ledger_category_scope.cpp @@ -1,18 +1,17 @@ // SPDX-License-Identifier: Apache-2.0 // -// An account, a budget and a category belong to one book (morph#373). +// An account, a budget and a category belong to one book. // // Accounts, categories and budgets each carry their own `ledger`, and three // client-facing actions join two of them by id alone: `SetCategory`, -// `LinkAccountToCategory` and `CreateBudget`. Until this file existed, none of -// them compared the two rows' books, so book two's account could be filed -// under book one's category and a book-one budget could report on a book-two -// category. Both rows are real, both ids are well-formed, and every id in this -// rung is a table-wide autoincrement -- so a lookup by id alone finds the -// foreign row and accepts it. +// `LinkAccountToCategory` and `CreateBudget`. Unless each compares the two +// rows' books, book two's account can be filed under book one's category and a +// book-one budget can report on a book-two category. Both rows are real, both +// ids are well-formed, and every id in this rung is a table-wide autoincrement +// -- so a lookup by id alone finds the foreign row and accepts it. // -// morph#382's ownership gate reaches all three sites and refuses a link across -// an *ownership* boundary. What it deliberately does not refuse -- and what +// The ownership gate reaches all three sites and refuses a link across an +// *ownership* boundary. What it deliberately does not refuse -- and what // every case below exercises -- is two books the **same** principal owns, and // two **unowned** books (the shape every `ledgers` row written before migration // `20260819000015` has, including every fixture book the scenario corpus @@ -21,7 +20,7 @@ // The rule these cases pin: an account and a category are the same book's, or // the link is refused; a budget's category is its own book's, or the budget is // refused. The refusal is `NotFound{": category does not belong to -// this ledger"}` -- `accountInLedger`'s idiom from morph#380, deliberately +// this ledger"}` -- `accountInLedger`'s own idiom, deliberately // distinct from `": no such account or category"`, because a client // that cannot tell them apart cannot tell a dead id from a mis-scoped one. // @@ -62,9 +61,9 @@ class ScopedPrincipal { morph::session::detail::ScopedContext _scope; }; -/// @brief A book with no recorded owner -- the shape every `ledgers` row had -/// before morph#382's migration, and the shape the scenario corpus's -/// fixture books still have. +/// @brief A book with no recorded owner -- the shape the scenario corpus's +/// fixture books have, and the only shape a `ledgers` row can still be +/// written in now that `CreateLedger` stamps its caller. [[nodiscard]] ledger::LedgerId unownedBook(Lightweight::DataMapper& mapper, const std::string& name) { ledger::db::LedgerRecord row; row.name = Light::SqlAnsiString<128>{name}; @@ -167,8 +166,8 @@ TEST_CASE("SetCategory refuses a category from the caller's other book", "[ledge twoBooks(ledgerModel, budgetModel, ledgerModel.execute(ledger::CreateLedger{.name = "Alice book one"}).id, ledgerModel.execute(ledger::CreateLedger{.name = "Alice book two"}).id); - // Both books are Alice's, so morph#382's ownership gate lets this through - // and only the book-scope check can refuse it. + // Both books are Alice's, so the ownership gate lets this through and only + // the book-scope check can refuse it. try { ledgerModel.execute(ledger::SetCategory{.accountId = books.secondSpend, .categoryId = books.firstCategory, @@ -239,7 +238,7 @@ TEST_CASE("CreateBudget refuses a category from another book", "[ledger][scope]" } TEST_CASE("Two unowned books may not be cross-linked either", "[ledger][scope]") { - // The case morph#382's ownership gate deliberately admits: a NULL owner + // The case the ownership gate deliberately admits: a NULL owner // means "created before ownership existed", so every principal passes the // ownership check on both books and the scope check is the only refusal // left. This is also the shape the scenario corpus seeds, so it is the diff --git a/examples/ledger/tests/test_ledger_import.cpp b/examples/ledger/tests/test_ledger_import.cpp index 9da31e147..ed5827790 100644 --- a/examples/ledger/tests/test_ledger_import.cpp +++ b/examples/ledger/tests/test_ledger_import.cpp @@ -351,10 +351,10 @@ TEST_CASE("ImportLedgerChunk rejects a chunk that does not sum to zero within a TEST_CASE("ImportLedgerChunk refuses an account from another book", "[ledger][import][security]") { // Both of this action's account lookups -- the chunk-wide - // `counterAccountId` and the per-row `account_id` column -- resolved by - // id alone, so a chunk imported into book one could post onto book two's - // accounts (morph#367). Every other test in this file uses a single book, - // which is why a second one is seeded here. + // `counterAccountId` and the per-row `account_id` column -- would resolve + // by id alone without the scope check, so a chunk imported into book one + // could post onto book two's accounts. Every other test in this file uses a + // single book, which is why a second one is seeded here. morph::ladder::testkit::DbFixture fixture; Lightweight::DataMapper mapper; ledger::db::LedgerRecord firstBookRow; diff --git a/examples/ledger/tests/test_ledger_list_transactions.cpp b/examples/ledger/tests/test_ledger_list_transactions.cpp index bfb034efe..bbfbb0c9a 100644 --- a/examples/ledger/tests/test_ledger_list_transactions.cpp +++ b/examples/ledger/tests/test_ledger_list_transactions.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 // -// `ListTransactions` -- the read that makes a journal id nameable (morph#428). +// `ListTransactions` -- the read that makes a journal id nameable. // // Before this action, `JournalId` appeared in exactly one DTO field in the // whole rung, and that field was `UndoTransaction`'s own *input*. @@ -71,9 +71,9 @@ class ScopedPrincipal { morph::session::detail::ScopedContext _scope; }; -/// @brief A book with no recorded owner -- the shape every `ledgers` row had -/// before morph#382's migration, written the only way one can still be -/// written now that `CreateLedger` always stamps its caller. +/// @brief A book with no recorded owner -- written the only way one can be, +/// now that `CreateLedger` always stamps its caller: by inserting the +/// row directly, as the scenario corpus's fixtures do. /// @param mapper The mapper to write through. /// @param name The book's name. /// @return The new book's id. @@ -274,7 +274,7 @@ TEST_CASE("A listed id drives UndoTransaction and restores the exact balances", TEST_CASE("ListTransactions refuses a principal that does not own the book", "[ledger][list-transactions][ownership]") { - // The gate every other book-reaching read carries (morph#382). A listing + // The gate every other book-reaching read carries. A listing // of a book's entries is precisely the read it exists for: without it, a // second authenticated client learns every description and amount in a // book it has nothing to do with. diff --git a/examples/ledger/tests/test_ledger_model.cpp b/examples/ledger/tests/test_ledger_model.cpp index 2048e5f98..1482d7fad 100644 --- a/examples/ledger/tests/test_ledger_model.cpp +++ b/examples/ledger/tests/test_ledger_model.cpp @@ -40,7 +40,7 @@ class ScopedPrincipal { } // namespace TEST_CASE("CreateLedger bootstraps a book the rest of the action surface can use", "[ledger][model]") { - // The whole point of `CreateLedger` (morph#361): every step below goes + // The whole point of `CreateLedger`: every step below goes // through the action surface, so this is exactly what an out-of-process // client can do against an empty database. No `mapper.Create` anywhere -- // if this test ever needs one again, the bootstrap gap is back. @@ -111,7 +111,7 @@ TEST_CASE("OpenAccount creates an account visible in GetLedger", "[ledger][model morph::ladder::testkit::DbFixture fixture; Lightweight::DataMapper mapper; // Seeds the `ledgers` row directly rather than dispatching `CreateLedger` - // (which exists since morph#361, and has its own cases above): this case + // (which has its own cases above): this case // is about `OpenAccount`, and a direct row keeps it from failing for a // reason that belongs to a different action. ledger::db::LedgerRecord ledgerRow; @@ -673,9 +673,9 @@ TEST_CASE("UndoTransaction produces an exact negation that re-passes zero-sum an // ───────────────────────────────────────────────────────────────────────── // Per-currency scale: a leg's `decimalPlaces` is the scale its numerator is // expressed in, so two legs at different scales are not comparable until -// both are restated at the account currency's own scale. These two cases -// are the ones morph#304 §A1 predicted; both are checked through the real -// `StoreTransaction` path, not against the partitioning helper directly. +// both are restated at the account currency's own scale. Both cases below go +// through the real `StoreTransaction` path, not against the partitioning helper +// directly. // ───────────────────────────────────────────────────────────────────────── TEST_CASE("StoreTransaction rejects legs that balance only because their scales differ", "[ledger][model]") { @@ -920,7 +920,7 @@ TEST_CASE("StoreTransaction refuses a leg on another book's account", "[ledger][ // a well-formed number naming a real row and a lookup by id alone finds // it. Without the ledger filter the entry is accepted, the journal is // filed under book one, and book two's balance moves with no journal of - // its own to explain it (morph#367). + // its own to explain it. morph::ladder::testkit::DbFixture fixture; Lightweight::DataMapper mapper; ledger::db::LedgerRecord firstBookRow; diff --git a/examples/ledger/tests/test_ledger_model_keys.cpp b/examples/ledger/tests/test_ledger_model_keys.cpp index ee0ef0f25..c15226454 100644 --- a/examples/ledger/tests/test_ledger_model_keys.cpp +++ b/examples/ledger/tests/test_ledger_model_keys.cpp @@ -1,9 +1,9 @@ // examples/ledger/tests/test_ledger_model_keys.cpp // SPDX-License-Identifier: Apache-2.0 // -// The three ledger models' primary keys, after morph#183 replaced their -// hand-written `ModelKeyTraits`/`ActionKeyTraits` specialisations with -// `BRIDGE_MODEL_KEY`/`BRIDGE_KEY_FROM`. +// The three ledger models' primary keys, as +// `BRIDGE_MODEL_KEY`/`BRIDGE_KEY_FROM` declare them -- rather than the +// hand-written `ModelKeyTraits`/`ActionKeyTraits` specialisations they replace. // // Nothing here needs a database: `ActionKeyTraits::key()` is a pure // function over the action's own fields, and that is the whole point -- key @@ -42,8 +42,8 @@ namespace { /// @brief Ids the key encoding has to survive intact, including one past -/// `2^53` -- the range morph#286 had to repair elsewhere in the -/// ladder, and the one a key routed through a double would corrupt. +/// `2^53` -- the range a key routed through a double corrupts, and the +/// one the ladder's QML boundary has to be careful with elsewhere. constexpr std::int64_t kIds[] = {1, 7, 4294967297, 9007199254740993}; } // namespace @@ -54,16 +54,16 @@ constexpr std::int64_t kIds[] = {1, 7, 4294967297, 9007199254740993}; TEST_CASE("LedgerModel keeps std::int64_t as its PrimaryKey because its keyed actions carry two id types", "[ledger][model][key]") { - // A regression guard, not a migration check: this was `std::int64_t` - // before morph#183 and still is. `BRIDGE_MODEL_KEY` would have deduced + // A regression guard: the key type is `std::int64_t` and has to stay one. + // `BRIDGE_MODEL_KEY` would have deduced // `LedgerId` from `&OpenAccount::ledgerId`, which is the better default // -- but `GetReportStatus` keys this same model on a `ReportJobId` // (report_dto.hpp), so no single strong id is *the* key type here. The // model therefore declares the raw scalar in its own body, which // `PrimaryKeyOf` prefers over any deduced type (model_key.hpp's // `KeyTypeOf`), and every action uses `BRIDGE_KEY_FROM` purely for the - // unwrapping. `primary()`/`instances()` keep returning - // `std::int64_t`, so no call site had to move. + // unwrapping. `primary()`/`instances()` therefore return `std::int64_t`, + // which is what every call site here expects. CHECK((std::same_as, std::int64_t>)); } @@ -192,8 +192,7 @@ TEST_CASE("UpdateRule stays keyless: it carries a ruleId, not a ledgerId", "[led TEST_CASE("An action carrying an empty id fails key extraction instead of routing to a garbage instance", "[ledger][model][key]") { - // This is the failure the hand-written traits were exposed to and the one - // morph#183 calls out by name. Their bodies were + // This is the failure a hand-written trait is exposed to: its body is // `keyToString(*action.ledgerId)`, and `LedgerId::operator*` is // `return *value;` on a `std::optional` (core/types.hpp) -- undefined // behaviour for a disengaged id. On a plain libc++ that yields whatever diff --git a/examples/ledger/tests/test_ledger_presenter.cpp b/examples/ledger/tests/test_ledger_presenter.cpp index 23d4fa9d7..4a7cdbd7e 100644 --- a/examples/ledger/tests/test_ledger_presenter.cpp +++ b/examples/ledger/tests/test_ledger_presenter.cpp @@ -39,7 +39,7 @@ using morph::ladder::testkit::pumpUntil; } /// @brief Creates one ledger row directly, bypassing the action surface. -/// `CreateLedger` exists since morph#361 and is what a real client +/// `CreateLedger` is what a real client /// uses; these presenter cases seed the row instead so a presenter /// failure cannot be a `CreateLedger` failure wearing a disguise (same /// shape test_ledger_reports.cpp uses). diff --git a/examples/ledger/tests/test_ledger_qml_surface.cpp b/examples/ledger/tests/test_ledger_qml_surface.cpp index 10a79950f..922f17f62 100644 --- a/examples/ledger/tests/test_ledger_qml_surface.cpp +++ b/examples/ledger/tests/test_ledger_qml_surface.cpp @@ -64,41 +64,41 @@ TEST_CASE("Every ledger bridge exposes exactly the surface gui/qml binds, and no audit.bind(QStringLiteral("reportBridge"), reportBridge); audit.bindIn(QStringLiteral("ReportView.qml"), QStringLiteral("bridge"), reportBridge); - // ── The former backlog, worked off member by member (#239) ─────────── - // The first run of this audit reported 15 members these four bridges - // publish that no file under gui/qml/ bound: `busy`/`busyChanged` on all - // three of `LedgerQmlBridge`/`BudgetQmlBridge`/`RuleQmlBridge`, - // `ledgerBridge`'s `refresh`/`undoTransaction`, `budgetBridge`'s + // ── No exemption list, and why ──────────────────────────────────────── + // Fifteen members these four bridges publish are the ones an unbound audit + // naturally reports: `busy`/`busyChanged` on all three of + // `LedgerQmlBridge`/`BudgetQmlBridge`/`RuleQmlBridge`, `ledgerBridge`'s + // `refresh`/`undoTransaction`, `budgetBridge`'s // `categoryCreated`/`budgetCreated`/`limitSet`/`lastBudgetId`/ // `linkAccount`, and `ruleBridge`'s `ruleCreated`/`ruleUpdated`. Every one - // was live, meaningful surface backed by real presenter state (`busy()` + // is live, meaningful surface backed by real presenter state (`busy()` // forwards to `_presenter.busy()` on all three; the others are real // create/link/undo gestures with a model and a presenter behind them) — - // none was dead scaffolding, so all 15 were bound rather than deleted: + // none is dead scaffolding, so all fifteen are bound rather than exempted: // - // * `busy` now gates a `BusyIndicator` in `LedgerView.qml`, + // * `busy` gates a `BusyIndicator` in `LedgerView.qml`, // `BudgetView.qml` and `RulesView.qml`. Its `busyChanged` NOTIFY needs // no exemption of its own: the audit treats a property's NOTIFY as // covered by reading the property (testkit_src/qml_surface.cpp's signal - // sweep), and that is exactly what these three views now do. + // sweep), and that is exactly what these three views do. // * `ledgerBridge.refresh` is a "Refresh" button; `undoTransaction` is a // journal-id field plus an "Undo" button, both in `LedgerView.qml`. // * `budgetBridge.linkAccount` is an account-id/category-id pair plus a // button in `BudgetView.qml`; `lastBudgetId` fills the same // chain-without-a-round-trip label next to "Create budget" that - // `lastCategoryId` already had next to "Create category". + // `lastCategoryId` fills next to "Create category". // * `categoryCreated`/`budgetCreated`/`limitSet` and `ruleCreated`/ // `ruleUpdated` carry no payload of their own — the state they // describe is already bound (`lastCategoryId`/`lastBudgetId`/ // `lastRule`) — so each gets a `Connections` handler that writes a // status line, the same shape bank's `Main.qml` uses for - // `txns.posted`/`payees.paid` (#303): a transient confirmation is the + // `txns.posted`/`payees.paid` for: a transient confirmation is the // only way a fire-and-forget signal becomes visible at all. // - // The list is checked in both directions: an exemption for a member that - // has since been deleted, or one QML has since bound, fails this test - // (testkit/qml_surface.hpp). It can only shrink deliberately, and with - // every one of the 15 now bound, there is nothing left to exempt. + // An exemption list would be checked in both directions -- an exemption for + // a member that has been deleted, or one QML has since bound, fails this + // test (testkit/qml_surface.hpp) -- but with all fifteen bound there is + // nothing to exempt. const QStringList findings = audit.run(); INFO(findings.join(QStringLiteral("\n")).toStdString()); diff --git a/examples/ledger/tests/test_ledger_reports.cpp b/examples/ledger/tests/test_ledger_reports.cpp index 84bd3f1b8..546e049b7 100644 --- a/examples/ledger/tests/test_ledger_reports.cpp +++ b/examples/ledger/tests/test_ledger_reports.cpp @@ -36,16 +36,15 @@ class ScopedPrincipal { /// `ledger::app::App`'s runner does -- under the runner principal, /// as an ordinary `RunReportJob` dispatch. /// -/// This file used to poll `GetReportStatus` in a bounded retry loop with -/// `std::this_thread::sleep_for` between iterations, because -/// `execute(SubmitReport)` posted the aggregation to a real -/// `ThreadPoolExecutor` the model itself owned and there was no way to -/// observe -- let alone control -- when the worker ran. There is no loop, no -/// sleep and no cap any more, and not because a test double was introduced: -/// the aggregation is now an ordinary synchronous action -/// (morph#160), so "the report has been computed" is simply what this call -/// returning means. `examples/TESTING.md`'s ban on `sleep_for` outside -/// `pump.hpp` is satisfied by construction here rather than by budget. +/// No retry loop, no `std::this_thread::sleep_for` and no cap -- and not +/// because a test double was introduced. The aggregation is an ordinary +/// synchronous action, so "the report has been computed" is simply what this +/// call returning means. Were `execute(SubmitReport)` to post the aggregation +/// to a `ThreadPoolExecutor` the model itself owned, there would be no way to +/// observe -- let alone control -- when the worker ran, and a caller would be +/// back to polling `GetReportStatus` on a budget. `examples/TESTING.md`'s ban +/// on `sleep_for` outside `pump.hpp` is satisfied by construction here rather +/// than by budget. /// /// The principal scope is installed and dropped inside this helper, so a /// caller's own `ScopedPrincipal` (a *user*, which `RunReportJob` refuses) @@ -267,10 +266,10 @@ TEST_CASE("GetReportStatus rejects a disengaged jobId and an unknown job", "[led } TEST_CASE("A submitted report stays Pending for as long as nothing runs it", "[ledger][reports]") { - // The property that says the executor really did leave the model - // (morph#160), and the one the old thread-pool version could not assert - // at all: with no runner anywhere in the process, a submitted job is - // stable at Pending rather than merely "not done yet". This is also + // The property that says the executor really is outside the model, and one + // a thread-pool-owning model could not express at all: with no runner + // anywhere in the process, a submitted job is stable at Pending rather than + // merely "not done yet". This is also // exactly what a job outliving the process that accepted it looks like -- // the row waits for whichever runner comes along next. morph::ladder::testkit::DbFixture fixture; @@ -301,11 +300,11 @@ TEST_CASE("A submitted report stays Pending for as long as nothing runs it", "[l TEST_CASE("Running one report job settles that job and no other", "[ledger][reports]") { // Two jobs outstanding at once, settled one at a time -- a completion // that wrote the wrong row, or every row, would show up here and nowhere - // else. The property predates morph#160 as a goal but could not be - // asserted while the aggregation was a lambda on a real pool: both jobs - // finished before either could be observed, and "job B is Done" looked - // the same whether B ran or A settled it. `RunReportJob` names the job it - // settles, so the second one staying Pending is now an ordinary CHECK. + // else. It is only assertable because the aggregation is synchronous: were + // it a lambda on a real pool, both jobs would finish before either could be + // observed, and "job B is Done" would look the same whether B ran or A + // settled it. `RunReportJob` names the job it settles, so the second one + // staying Pending is an ordinary CHECK. morph::ladder::testkit::DbFixture fixture; Lightweight::DataMapper mapper; ledger::db::LedgerRecord ledgerRow; @@ -487,16 +486,16 @@ TEST_CASE("RunReportJob rejects unengaged ids and an unknown job", "[ledger][rep } TEST_CASE("RunReportJob refuses a job that belongs to another ledger", "[ledger][reports]") { - // morph#371. The action carries both a `jobId` and a `ledgerId`, and used - // to resolve the job by id alone -- the ledger guard below it checked only - // that `action.ledgerId` named an *existing* book, never that it was - // *this job's* book. The aggregation then ran against the action's ledger - // (`computeReportJson(mapper, action.ledgerId, ...)`) and settled the + // The action carries both a `jobId` and a `ledgerId`. Resolving the job by + // id alone is not enough: the ledger guard checks only that + // `action.ledgerId` names an *existing* book, never that it is *this job's* + // book. The aggregation runs against the action's ledger + // (`computeReportJson(mapper, action.ledgerId, ...)`) and settles the // action's job (`finishReportJob(mapper, *action.jobId, Done, ...)`), so - // book two's job settled `Done` carrying book one's totals, and a - // subsequent `GetReportStatus` handed those back as book two's report. - // `Done` is terminal, so the correct body could never afterwards be - // computed for that job. + // without the comparison below book two's job settles `Done` carrying book + // one's totals and a subsequent `GetReportStatus` hands those back as book + // two's report. `Done` is terminal, so the correct body could never + // afterwards be computed for that job. // // The two books are given different currencies so a cross-filed body is // visible in the body itself rather than only in the status: every diff --git a/examples/ledger/tests/test_report_presenter.cpp b/examples/ledger/tests/test_report_presenter.cpp index 2bfe0690f..5343d0689 100644 --- a/examples/ledger/tests/test_report_presenter.cpp +++ b/examples/ledger/tests/test_report_presenter.cpp @@ -9,7 +9,7 @@ // It is also the only test in this rung that runs a client and a // `ledger::app::App` together, which is what the shipped deployment actually // is. That became load-bearing rather than incidental when the report job -// moved out of the model (morph#160): `SubmitReport` now only writes a +// moved out of the model: `SubmitReport` now only writes a // Pending row, so without an App ticking somewhere in the process the // client's poller would poll a job that nothing will ever settle. Everything // below is unchanged from when the model computed the report inside its own @@ -46,7 +46,7 @@ using namespace std::chrono_literals; /// installed principal dangling, which surfaces as "mutating action /// dispatched with an empty principal" rather than as a crash. Same /// shape as test_ledger_reports.cpp's own helper, and the same hazard -/// as morph#137 -- a reference outliving what it refers to. +/// as any reference outliving what it refers to. class ScopedPrincipal { public: explicit ScopedPrincipal(std::string principal) : _ctx{makeContext(std::move(principal))}, _scope{_ctx} {} From 62deb411f753bc9aa912f222b5589e53a9ef554a Mon Sep 17 00:00:00 2001 From: Yaraslau Tamashevich Date: Thu, 24 Sep 2026 03:12:10 +0200 Subject: [PATCH 05/10] comments(examples/kanban): state the constraint, drop the citation README, the two schema-driven QML views, board_model.{hpp,cpp} and ten test suites. `gui/qml/BoardView.qml` and `tests/test_board_layout.cpp` are left alone -- an open PR holds them. Kept whole: the post-commit-tail argument in board_model.cpp (why the four re-reads moved inside the transaction rather than being shielded, and why a re-read failing after the commit leaves nothing truthful to return), the 165 ThreadSanitizer warnings behind test_kanban_stress.cpp's Qt-free rule and why a prebuilt Qt makes them unusable either way, and the `WaitBudget`/`WaitStep` argument against a NOLINT. The README's "Findings" section was a list of tracker links. It now records what each finding *is*, and says the tracker holds their state -- which is the part a reader of this file cannot get from the link anyway. Catch2 tags (`[kanban][board][morph#751]` and friends) are left as they are: renaming a tag is a code change, not a comment one. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW --- examples/kanban/README.md | 69 ++++++++---------- examples/kanban/gui/qml/LoginView.qml | 13 ++-- examples/kanban/gui/qml/MembersView.qml | 7 +- examples/kanban/gui/qml/RulesView.qml | 9 ++- examples/kanban/gui_lib/board_qml_bridge.cpp | 2 +- examples/kanban/gui_lib/board_qml_bridge.hpp | 4 +- examples/kanban/gui_lib/kanban_schemas.hpp | 24 +++---- examples/kanban/include/kanban/core/types.hpp | 6 +- .../include/kanban/models/board_model.hpp | 48 ++++++------- examples/kanban/src/db/schema.cpp | 8 +-- examples/kanban/src/models/board_model.cpp | 71 ++++++++++--------- examples/kanban/tests/test_board_dto.cpp | 6 +- examples/kanban/tests/test_board_model.cpp | 54 +++++++------- .../test_board_model_journal_outcome.cpp | 46 ++++++------ .../tests/test_board_post_commit_tail.cpp | 40 +++++------ .../kanban/tests/test_board_presenter.cpp | 2 +- .../kanban/tests/test_board_qml_bridge.cpp | 4 +- .../kanban/tests/test_board_unattached.cpp | 56 +++++++-------- .../kanban/tests/test_gui_forms_render.cpp | 35 +++++---- examples/kanban/tests/test_kanban_offline.cpp | 2 +- .../kanban/tests/test_kanban_qml_surface.cpp | 44 ++++++------ examples/kanban/tests/test_kanban_stress.cpp | 64 ++++++++--------- .../tests/test_project_admin_qml_bridge.cpp | 2 +- 23 files changed, 297 insertions(+), 319 deletions(-) diff --git a/examples/kanban/README.md b/examples/kanban/README.md index 99f002d9c..00361938b 100644 --- a/examples/kanban/README.md +++ b/examples/kanban/README.md @@ -185,20 +185,18 @@ be written here. Two elements remain, both rule 2(a) — "the generated UI | `gui/qml/BoardView.qml`'s drag-and-drop board | `MoveTaskPosition` | **(a).** A drag is a gesture, not a form. See below. | | `gui/qml/MembersView.qml`'s per-row role `ComboBox` (`:78`) | `SetMemberRole` | **(a).** Edits one field of an existing row in place, on selection change, with no Submit gesture — not the create-a-new-thing shape `DynamicForm` renders. See below. | -**The enum-rendering gap this table used to cite is closed.** -`morph::forms::schemaJson` describes a C++ `enum class` completely — `Role` -emits as `{"type":"string","oneOf":[{"title":"Viewer","const":"Viewer"},…]}` — -and `DynamicForm` now draws that as a combo box, refusing a value outside the -set ([#386](https://github.com/LASTRADA-Software/morph/issues/386)). Both -`SetMemberRole` and `CreateRule` render as ordinary schema-driven forms as a -result ([#393](https://github.com/LASTRADA-Software/morph/issues/393)): -`gui/qml/MembersView.qml`'s "add member" row and `gui/qml/RulesView.qml`'s +**There is no enum-rendering gap.** `morph::forms::schemaJson` describes a C++ +`enum class` completely — `Role` emits as +`{"type":"string","oneOf":[{"title":"Viewer","const":"Viewer"},…]}` — and +`DynamicForm` draws that as a combo box, refusing a value outside the set. Both +`SetMemberRole` and `CreateRule` therefore render as ordinary schema-driven +forms: `gui/qml/MembersView.qml`'s "add member" row and `gui/qml/RulesView.qml`'s "add rule" row are both `DynamicForm`s now, and `CreateRule::triggerColumnId` is a `morph::forms::Choice<…, "GetBoardState">` (rule 3's shape for a user-chosen foreign key), fetched by the renderer itself through `BoardBridge::fetchOptions`. Only `MembersView`'s **per-row** role picker — editing an existing member in place, not adding one — stays -hand-built, on different grounds than the enum gap: it is an inline-edit +hand-built, on grounds of its own: it is an inline-edit interaction, not a form, the same reasoning the drag-and-drop board below already gives for its own gesture. @@ -217,11 +215,9 @@ schema — which is why nothing is filed against `morph::forms` for it. Two smaller findings fell out of rendering these forms for real, both filed rather than folded in: every `DynamicForm` warns about `onOptionsReceived` -against any controller with no `Choice` field -([#387](https://github.com/LASTRADA-Software/morph/issues/387)), and the -renderer mistypes array-valued schema keys when `schema` is assigned as a -`QVariant` instead of bound -([#388](https://github.com/LASTRADA-Software/morph/issues/388)). +against any controller with no `Choice` field, and the renderer mistypes +array-valued schema keys when `schema` is assigned as a `QVariant` instead of +bound. Both are in "Findings" below. ## Expected strain points @@ -262,7 +258,7 @@ renderer mistypes array-valued schema keys when `schema` is assigned as a pool=4, 32 boards writing concurrently, WAL on and off; measure throughput collapse; assert no timeout-then-committed double-apply. - **The queue depth bound exists in the framework; this rung has not - adopted it.** The framework gap this rung filed as morph#112 is closed: + adopted it.** There is no framework gap left: `IOfflineQueue` enforces a reject-newest overflow policy — `maxDepth()` (`include/morph/offline/offline_queue.hpp:192`) reports the configured capacity, `enqueue()` throws `OfflineQueueFullError` (`:80`) rather than @@ -316,34 +312,29 @@ reads by the caller's project role (not bearer-token validity alone). ## Findings -Filed as GitHub issues, per [`AGENTS.md`](../../AGENTS.md)'s filing bar: +Filed on the tracker, per [`AGENTS.md`](../../AGENTS.md)'s filing bar. What each +one *is* is recorded here; the tracker holds their state: -- [#343](https://github.com/LASTRADA-Software/morph/issues/343) - — the replay-attempt budget cannot tell an undelivered replay from a - rejected one, so reconnect flaps dead-letter work the server never saw. -- [#344](https://github.com/LASTRADA-Software/morph/issues/344) — - the flagship GUI was hand-built with no `morph::forms` usage and no rule-2 - justification. Seven forms now render through the shipped renderer and the - two remaining hand-built elements each carry their written rule-2(a) - justification (see "morph subsystems exercised"). -- [#386](https://github.com/LASTRADA-Software/morph/issues/386) — - `DynamicForm` rendered a C++ `enum class` as a free-text field, which is - what kept `MembersView`/`RulesView`'s add-member/add-rule forms hand-built - until it was fixed; both now render through the shipped renderer - ([#393](https://github.com/LASTRADA-Software/morph/issues/393)). -- [#387](https://github.com/LASTRADA-Software/morph/issues/387) — - every `DynamicForm` warns about `onOptionsReceived` on a controller with no +- The replay-attempt budget cannot tell an undelivered replay from a rejected + one, so reconnect flaps dead-letter work the server never saw. +- Every `DynamicForm` warns about `onOptionsReceived` on a controller with no `Choice` field. -- [#388](https://github.com/LASTRADA-Software/morph/issues/388) — - `DynamicForm` mistypes array-valued schema keys when `schema` is assigned as +- `DynamicForm` mistypes array-valued schema keys when `schema` is assigned as a `QVariant` rather than bound. -Not filed here, deliberately: the applied-ops ledger that "Exactly-once has -no owner in the stack" (below) forces every rung to rebuild is already -morph#226, which records the pattern as past -[`IMPLEMENTATION.md`](../IMPLEMENTATION.md)'s rule-of-three threshold — this -rung's `AppliedOpRecord` is one of its occurrences, not a separate gap. The -ladder-wide sweep that prompted this README's truth pass is morph#304. +Closed, and kept here because the current shape is the answer to them: the GUI +being hand-built with no `morph::forms` usage and no rule-2 justification — +seven forms render through the shipped renderer now, and the two remaining +hand-built elements each carry their written rule-2(a) justification (see "morph +subsystems exercised") — and `DynamicForm` rendering a C++ `enum class` as a +free-text field, which is what would keep `MembersView`/`RulesView`'s +add-member/add-rule forms hand-built. + +Not filed here, deliberately: the applied-ops ledger that "Exactly-once has no +owner in the stack" (below) forces every rung to rebuild is already filed as one +pattern past [`IMPLEMENTATION.md`](../IMPLEMENTATION.md)'s rule-of-three +threshold — this rung's `AppliedOpRecord` is one of its occurrences, not a +separate gap. ## Definition of done diff --git a/examples/kanban/gui/qml/LoginView.qml b/examples/kanban/gui/qml/LoginView.qml index cd0befd5d..2d5021fdd 100644 --- a/examples/kanban/gui/qml/LoginView.qml +++ b/examples/kanban/gui/qml/LoginView.qml @@ -7,13 +7,12 @@ // (examples/IMPLEMENTATION.md rule 2, "schema-driven forms only"). If `Login` // ever grows a second field, this file does not change. // -// This screen used to be a hand-built `TextField` + `Button`, on the strength -// of this rung's GUI design spec -// (docs/superpowers/specs/2026-08-17-kanban-gui-design.md §4) — which settles -// the two-bridge/property-bag architecture and says nothing about forms at -// all, so it never justified the exception rule 2 requires. See morph#344, and -// the rung README's "morph subsystems exercised" section for what is still -// hand-built here and what that costs. +// A hand-built `TextField` + `Button` would need rule 2's written exception, +// and this rung's GUI design spec +// (docs/superpowers/specs/2026-08-17-kanban-gui-design.md §4) does not supply +// one: it settles the two-bridge/property-bag architecture and says nothing +// about forms at all. See the rung README's "morph subsystems exercised" +// section for what is still hand-built here and what that costs. // // `projectAdminBridge` defaults to null so this same file also loads with // nothing wired up, which is exactly what the offscreen engine-load smoke diff --git a/examples/kanban/gui/qml/MembersView.qml b/examples/kanban/gui/qml/MembersView.qml index cfb4d81f8..43f4839c3 100644 --- a/examples/kanban/gui/qml/MembersView.qml +++ b/examples/kanban/gui/qml/MembersView.qml @@ -16,10 +16,9 @@ // different and worse interaction than the inline picker it would replace. // This is examples/IMPLEMENTATION.md rule 2's justification (a) on the // row-editing interaction, the same grounds BoardView.qml's header comment -// gives for why MoveTaskPosition's drag gesture stays hand-built -- not the -// enum-rendering gap morph#386 fixed, which this file no longer has (the "add -// member" row's role picker is now the schema-driven combo box DynamicForm -// draws for `SetMemberRole::role`'s closed set). See morph#393. +// gives for why MoveTaskPosition's drag gesture stays hand-built. It is not an +// enum-rendering gap: the "add member" row's role picker is the schema-driven +// combo box DynamicForm draws for `SetMemberRole::role`'s closed set. // // `projectAdminBridge` defaults to null and `projectId` defaults to -1 so // this same file also loads standalone with nothing wired up, which is diff --git a/examples/kanban/gui/qml/RulesView.qml b/examples/kanban/gui/qml/RulesView.qml index 1ddb278d7..b9f01e834 100644 --- a/examples/kanban/gui/qml/RulesView.qml +++ b/examples/kanban/gui/qml/RulesView.qml @@ -9,11 +9,10 @@ // shipped MorphForms DynamicForm) -- no "watch for other trigger kinds" // affordance, since RuleTriggerEvent has exactly one member (rule_dto.hpp). // -// Both of this rung's remaining hand-built forms converted together -// (morph#393): `CreateRule::mutationType` (a `RuleMutationType` enum class) -// now renders as the schema-driven combo box DynamicForm draws for a closed -// `oneOf`-of-`const`s set (morph#386 closed the gap that used to force this -// to a free-text field), and `CreateRule::triggerColumnId` moved from a raw +// Both of this rung's remaining hand-built forms are schema-driven: +// `CreateRule::mutationType` (a `RuleMutationType` enum class) +// renders as the combo box DynamicForm draws for a closed +// `oneOf`-of-`const`s set, and `CreateRule::triggerColumnId` is a // `ColumnId` to a `morph::forms::Choice<…, "GetBoardState">` (rule 3's shape // for a user-chosen foreign key) -- so the trigger-column combo box is now // server-fetched by DynamicForm itself, via BoardBridge.fetchOptions(), rather diff --git a/examples/kanban/gui_lib/board_qml_bridge.cpp b/examples/kanban/gui_lib/board_qml_bridge.cpp index 579313b24..4f99888ea 100644 --- a/examples/kanban/gui_lib/board_qml_bridge.cpp +++ b/examples/kanban/gui_lib/board_qml_bridge.cpp @@ -75,7 +75,7 @@ using ::morph::ladder::gui::idNumber; /// happen here explicitly. /// /// Kept as its own function rather than collapsed into -/// `morph::ladder::gui::idFromText` (morph#169): the shared helper +/// `morph::ladder::gui::idFromText`: the shared helper /// maps text to id and stops there, deliberately treating `0` as an /// engaged id, because a helper that folded a value into the empty /// state would be the inbound half of the unset-vs-zero collapse that diff --git a/examples/kanban/gui_lib/board_qml_bridge.hpp b/examples/kanban/gui_lib/board_qml_bridge.hpp index b611af5d8..9dec2e963 100644 --- a/examples/kanban/gui_lib/board_qml_bridge.hpp +++ b/examples/kanban/gui_lib/board_qml_bridge.hpp @@ -282,8 +282,8 @@ class BoardBridge : public QObject { /// `mutationValue`." Manager-only. Emits `ruleCreated`, or `failed`. /// /// Not `Q_INVOKABLE` — see `createColumn`: `RulesView.qml` submits - /// `CreateRule` through the schema renderer now (morph#393), so nothing in - /// `gui/qml/` calls this either. + /// `CreateRule` through the schema renderer, so nothing in `gui/qml/` calls + /// this either. /// @param triggerColumnId The triggering column, as its plain number. /// @param mutationType `"AddTag"` or `"RemoveTag"`. /// @param mutationValue The tag name the mutation adds or removes. diff --git a/examples/kanban/gui_lib/kanban_schemas.hpp b/examples/kanban/gui_lib/kanban_schemas.hpp index 7971fafaf..feac6eff5 100644 --- a/examples/kanban/gui_lib/kanban_schemas.hpp +++ b/examples/kanban/gui_lib/kanban_schemas.hpp @@ -28,18 +28,18 @@ /// action a controller does not serve is reported, not silently dropped: see /// each bridge's `submitIfValid`. /// -/// **What used to be deliberately absent.** `SetMemberRole` and `CreateRule` -/// were held back here because each carried a C++ `enum class` member -/// (`Role`, `RuleMutationType`) that `schemaJson` emitted as a closed `oneOf` -/// of `const`s and the shipped `DynamicForm` rendered as a **free-text -/// field**, accepting and submitting any string at all (morph#386). That gap -/// is closed — `DynamicForm` now draws a closed `oneOf`-of-`const`s enum as a -/// combo box and refuses a value outside the set — so both actions render -/// here like every other form (morph#393). `CreateRule::triggerColumnId` also -/// moved from a raw `ColumnId` to a `morph::forms::Choice<…, -/// "GetBoardState">`, the shape rule 3 prescribes for a user-chosen foreign -/// key; `GetBoardState`'s reply returns `columns` as its first array member. -/// `MoveTaskPosition` stays absent for the opposite reason: it is a drag +/// **What a renderer has to support for this list to be complete.** +/// `SetMemberRole` and `CreateRule` each carry a C++ `enum class` member +/// (`Role`, `RuleMutationType`) that `schemaJson` emits as a closed `oneOf` of +/// `const`s. A `DynamicForm` that drew that as a **free-text field** would +/// accept and submit any string at all, which is why neither action could be +/// listed here; the shipped renderer draws it as a combo box and refuses a value +/// outside the set, so both render like every other form. +/// `CreateRule::triggerColumnId` is a `morph::forms::Choice<…, +/// "GetBoardState">` rather than a raw `ColumnId` — the shape rule 3 prescribes +/// for a user-chosen foreign key; `GetBoardState`'s reply returns `columns` as +/// its first array member. +/// `MoveTaskPosition` is absent for a different reason: it is a drag /// gesture, not a form (rule 2(a); see the README). namespace kanban::gui { diff --git a/examples/kanban/include/kanban/core/types.hpp b/examples/kanban/include/kanban/core/types.hpp index a12a78d68..2d2aaa9e8 100644 --- a/examples/kanban/include/kanban/core/types.hpp +++ b/examples/kanban/include/kanban/core/types.hpp @@ -104,8 +104,8 @@ enum class Role : std::uint8_t { Viewer, Member, Manager }; /// The shape carries a constraint the type cannot enforce on its own: because /// `value == 0` *is* the "not entered" state, **an event id of `0` is /// unrepresentable** -- construct one and it reports `hasValue() == false`, -/// so a real event would read as "no event" (morph#215). The constraint holds -/// because these ids are SQLite row ids, which start at 1. `fromRowId()` is +/// so a real event would read as "no event". The constraint holds because these +/// ids are SQLite row ids, which start at 1. `fromRowId()` is /// the enforcement; use it for every conversion from a stored value. struct BoardEventId { std::int64_t value{0}; @@ -127,7 +127,7 @@ struct BoardEventId { if (rowId == 0) { throw KanbanError{ "BoardEventId::fromRowId: an event row id of 0 is unrepresentable -- 0 is this " - "type's \"not entered\" sentinel (morph#215)"}; + "type's \"not entered\" sentinel"}; } return BoardEventId{.value = rowId}; } diff --git a/examples/kanban/include/kanban/models/board_model.hpp b/examples/kanban/include/kanban/models/board_model.hpp index 02b1a7bf9..888ef719a 100644 --- a/examples/kanban/include/kanban/models/board_model.hpp +++ b/examples/kanban/include/kanban/models/board_model.hpp @@ -314,9 +314,9 @@ class BoardModel { /// `_projectIdStr` is also set to it. A key that is not one leaves /// `_projectIdStr` disengaged, so every `has_value()` attach guard /// in this class still refuses to serve a handler no `OpenBoard` - /// or canonical keyed attach ever named a board for (#368) -- + /// or canonical keyed attach ever named a board for -- /// independently of what @p entityKey was, since `_entityKeyStr`'s - /// assignment above no longer has any bearing on that guard. + /// assignment above has no bearing on that guard. void attachActionLog(std::shared_ptr<::morph::journal::IActionLog> log, std::string entityKey); private: @@ -368,20 +368,20 @@ class BoardModel { /// @brief Journals the exception **currently being handled** as an /// `Outcome::Failed` entry for @p action, whatever its type, and - /// contains any failure of the journalling itself (morph#757). + /// contains any failure of the journalling itself. /// /// Every mutating `execute()` overload ends `} catch (...) { - /// logFailureForCurrentException(action); throw; }`. Before - /// morph#757 they ended `catch (const KanbanError&)` instead, so - /// `Outcome::Failed` was written for this rung's own four refusal - /// types and for nothing else: an `std::invalid_argument` out of - /// `std::stoull`, one of Lightweight's SQL exceptions from a - /// pre-commit `Query`/`Create`/`Update`/`Delete` (a contended - /// `SQLITE_BUSY` past the busy timeout being the one observed in - /// CI, morph#566), or a `std::bad_alloc` all reached the caller - /// having journalled nothing at all. Nothing in "a rejected - /// attempt is itself audit-worthy" depends on the exception's - /// type; only the `catch` clause did. + /// logFailureForCurrentException(action); throw; }`. `catch (const + /// KanbanError&)` would not do: it writes `Outcome::Failed` for this + /// rung's own four refusal types and for nothing else, so an + /// `std::invalid_argument` out of `std::stoull`, one of Lightweight's + /// SQL exceptions from a pre-commit `Query`/`Create`/`Update`/ + /// `Delete` (a contended `SQLITE_BUSY` past the busy timeout is the + /// one that actually turns up under CI's parallelism), or a + /// `std::bad_alloc` would all reach the caller having journalled + /// nothing at all. Nothing in "a rejected attempt is itself + /// audit-worthy" depends on the exception's type, so neither does + /// this `catch` clause. /// /// **Why the containment is not optional.** This runs while the /// original exception is still in flight and the caller's own @@ -393,7 +393,7 @@ class BoardModel { /// *replace* the failure being reported with the failure to /// report it: a less diagnosable exception, and on a destructor /// path a `std::terminate`. So it is contained here, the same way - /// `runPostCommitTail` contains the mirror case (morph#751), and + /// `runPostCommitTail` contains the mirror case, and /// the exception the caller sees is always the original one. /// /// **Precondition:** an exception is being handled. This is a @@ -493,16 +493,16 @@ class BoardModel { /// whether the caller may touch it is `requireRole`'s. It holds /// only because both writers establish it: `OpenBoard` stores /// `std::to_string` of a real row id, and `attachActionLog` - /// declines any key that is not one. See #368 for what the - /// fifteen guards did while it did not hold. + /// declines any key that is not one. Break either writer and all + /// fifteen guards start passing a key that does not parse. std::optional _projectIdStr; /// @brief The key every subsequent `logAction`/`logFailure` call stamps /// as its `LogEntry::entityKey` -- kept as its own member, separate /// from `_projectIdStr` (the attach state), rather than one member /// serving both -- the same *shape* `ledger::LedgerModel:: - /// _entityKeyStr` (`ledger_model.cpp`) and lims/crm already use - /// (#422; #368's triage deferred exactly this split). Not the same + /// _entityKeyStr` (`ledger_model.cpp`) and lims/crm already use. + /// Not the same /// *assignment*: `LedgerModel` has no attach-state member to guard /// against at all (every one of its actions carries its own /// `ledgerId`, so nothing there plays `_projectIdStr`'s role), so @@ -565,12 +565,12 @@ BRIDGE_REGISTER_ACTION(kanban::BoardModel, kanban::RemoveAttachment, "RemoveAtta // is the action that names it. `BRIDGE_MODEL_KEY` deduces the key *type* from // the member it is handed, so `PrimaryKeyOf` is `kanban::ProjectId` // itself -- the strong id examples/IMPLEMENTATION.md rule 3 requires -- rather -// than the unwrapped `std::int64_t` this rung declared while -// `morph::model::ModelKey` still admitted only raw scalars (morph#163 widened -// it; morph#183 migrated this rung off the hand-written specialisations). +// than the unwrapped `std::int64_t` a hand-written specialisation would have to +// declare, `morph::model::ModelKey` admitting a strong id rather than only raw +// scalars. // -// The disengaged-`projectId` rejection the hand-written `key()` spelled out is -// now `morph::model::keyToString`'s own: it throws for a strong id with no +// The disengaged-`projectId` rejection a hand-written `key()` would spell out is +// `morph::model::keyToString`'s: it throws for a strong id with no // value instead of dereferencing an empty optional, which is what makes // `BoardBridge::openBoard("not-a-number")` (parsed into a default-constructed // `ProjectId{}` by board_qml_bridge.cpp's `parseId`) a rejected `Completion` diff --git a/examples/kanban/src/db/schema.cpp b/examples/kanban/src/db/schema.cpp index 5630268f3..40f05c235 100644 --- a/examples/kanban/src/db/schema.cpp +++ b/examples/kanban/src/db/schema.cpp @@ -9,7 +9,7 @@ namespace kanban::db { void setup(const std::string& connectionString) { - // morph#740: nothing in Lightweight stops a pooled DataMapper from being + // Nothing in Lightweight stops a pooled DataMapper from being // returned with a transaction still open on it -- `DataMapperPool::Return` // does no transaction cleanup, and the cost lands on the next, unrelated // borrower as a 60s stall and a `database is locked` it did not cause. @@ -29,9 +29,9 @@ void setup(const std::string& connectionString) { // All eight tables in one migration, in dependency order, matching // bookmarks'/polls' own single-migration schema.cpp. Bounded columns use // Varchar(N) matching their entity's SqlAnsiString capacity (Task 4); -// unbounded columns use NVarchar(0), never Text() -- the fix already applied -// to bookmarks (PR #90) and polls (PR #91) for this exact DDL/entity -// mismatch (design spec §7). +// unbounded columns use NVarchar(0), never Text() -- bookmarks' and polls' +// schema.cpp carry the same rule, for this exact DDL/entity mismatch +// (design spec §7). using namespace Lightweight::SqlColumnTypeDefinitions; diff --git a/examples/kanban/src/models/board_model.cpp b/examples/kanban/src/models/board_model.cpp index 4244f722e..03c470e0f 100644 --- a/examples/kanban/src/models/board_model.cpp +++ b/examples/kanban/src/models/board_model.cpp @@ -154,7 +154,7 @@ void requireTaskBelongsToProject(::Lightweight::DataMapper& mapper, const db::Pr /// handler is only ever attached to one board (`OpenBoard` is this /// model's keyed attach action), so an argument naming a *different* /// project cannot be served here: it is refused rather than quietly -/// answered from -- or written onto -- the attached board (morph#369). +/// answered from -- or written onto -- the attached board. /// @param projectId The action's own `projectId` field. /// @param attachedProjectDbId The attached board's project id, parsed from /// `_projectIdStr`. @@ -165,7 +165,7 @@ void requireProjectMatchesAttachedBoard(ProjectId projectId, std::uint64_t attac } /// @brief Runs @p tail, a handler's *post-commit* work, and contains any -/// exception it throws (morph#566). +/// exception it throws. /// /// A handler that has already called `SqlTransaction::Commit()` has made its /// mutation durable. Whatever it does afterwards -- journalling, a rule @@ -203,7 +203,7 @@ template /// @brief The same containment for a tail that produces nothing the caller /// needs -- the shape every mutating handler other than -/// `MoveTaskPosition` has (morph#751). +/// `MoveTaskPosition` has. /// /// Those handlers' tails are `logAction` alone: the value they return was /// already computed, so there is no fallback to choose and no second value to @@ -212,12 +212,12 @@ template /// distinguish, which is worse than no branch. This overload is that case /// written down. /// -/// **What each handler must do to be eligible**, and what the adoption in -/// morph#751 did: anything the *caller's return value* depends on runs before -/// `Commit()`, not after it. `CreateColumn` and its three siblings therefore -/// build their `GetBoardResult` inside the transaction now, which is where -/// `MoveTaskPosition` has always built its own (it needs one for the -/// applied-ops ledger row). That is not a workaround for this overload's lack +/// **What each handler must do to be eligible**: anything the *caller's return +/// value* depends on runs before `Commit()`, not after it. `CreateColumn` and +/// its three siblings therefore build their `GetBoardResult` inside the +/// transaction, which is where `MoveTaskPosition` builds its own (it needs one +/// for the applied-ops ledger row). That is not a workaround for this +/// overload's lack /// of a fallback -- it is the stronger ordering. A re-read that fails *before* /// the commit rolls the write back, so the caller's "this failed" is true; /// a re-read that fails *after* it leaves nothing truthful to return, because @@ -386,11 +386,11 @@ void BoardModel::attachActionLog(std::shared_ptr<::morph::journal::IActionLog> l // this condition is load-bearing, not defensive. `_projectIdStr` is the // answer to "which project is this handler attached to", which every // `execute()` overload asks as `has_value()` before dereferencing it into - // `std::stoull`; adopting the key unconditionally made that question - // answer "yes" for strings no `stoull` can parse, so the guards fell - // through and the rung replied with the bare text `stoull` -- an - // `std::invalid_argument` escaping as though it were a domain error - // (#368). See `attachActionLog`'s declaration for the contract this keeps. + // `std::stoull`. Adopting the key unconditionally makes that question + // answer "yes" for strings no `stoull` can parse, so the guards fall + // through and the rung replies with the bare text `stoull` -- an + // `std::invalid_argument` escaping as though it were a domain error. + // See `attachActionLog`'s declaration for the contract this keeps. if (namesAProject(entityKey)) { _projectIdStr = std::move(entityKey); } @@ -474,8 +474,9 @@ void BoardModel::logFailureForCurrentException(const Action& action) const { throw; } catch (const KanbanError& error) { // A refusal this rung decided on. `what()` is the message the - // caller is being given, and it is what the journal recorded for - // these before morph#757 -- unchanged, deliberately. + // caller is being given, and it goes into the journal unprefixed -- + // see the `std::exception` arm below for why only that one is + // prefixed. logFailure(action, error.what()); } catch (const std::exception& error) { // Not a refusal -- something broke. Prefixed rather than recorded @@ -534,8 +535,8 @@ GetBoardResult BoardModel::execute(const OpenBoard& action) { // string -- the value execute(GetActivity)'s own `_log->entries( // *_projectIdStr)` looks entries up by, so a session that opened its // board this way keeps finding the entries logAction/logFailure wrote - // during it once _entityKeyStr and _projectIdStr are no longer the same - // member (#422). + // during it, even though _entityKeyStr and _projectIdStr are separate + // members. _projectIdStr = std::to_string(project.id.Value()); _entityKeyStr = *_projectIdStr; return buildState(mapper.Get(), project); @@ -583,7 +584,7 @@ GetBoardResult BoardModel::execute(const CreateColumn& action) { event.createdAtMs = nowMs(); mapper->Create(event); - // Built before the commit, not after it (morph#751). The board state + // Built before the commit, not after it. The board state // is this call's whole return value, so a re-read that fails must roll // the write back rather than leave a committed mutation with nothing // truthful to report -- see `runPostCommitTail`'s void overload for the @@ -592,7 +593,7 @@ GetBoardResult BoardModel::execute(const CreateColumn& action) { transaction.Commit(); - // ── post-commit tail (morph#751) ──────────────────────────────── + // ── post-commit tail ──────────────────────────────────────────── // The row is durable from the line above; `logAction` is not, and // `_log->append`/`flush` can throw. A throw here must not tell the // caller the CreateColumn failed. @@ -635,7 +636,7 @@ GetBoardResult BoardModel::execute(const CreateSwimlane& action) { event.createdAtMs = nowMs(); mapper->Create(event); - // Built before the commit, not after it (morph#751). The board state + // Built before the commit, not after it. The board state // is this call's whole return value, so a re-read that fails must roll // the write back rather than leave a committed mutation with nothing // truthful to report -- see `runPostCommitTail`'s void overload for the @@ -644,7 +645,7 @@ GetBoardResult BoardModel::execute(const CreateSwimlane& action) { transaction.Commit(); - // ── post-commit tail (morph#751) ──────────────────────────────── + // ── post-commit tail ──────────────────────────────────────────── // The row is durable from the line above; `logAction` is not, and // `_log->append`/`flush` can throw. A throw here must not tell the // caller the CreateSwimlane failed. @@ -702,7 +703,7 @@ GetBoardResult BoardModel::execute(const CreateTask& action) { event.createdAtMs = nowMs(); mapper->Create(event); - // Built before the commit, not after it (morph#751). The board state + // Built before the commit, not after it. The board state // is this call's whole return value, so a re-read that fails must roll // the write back rather than leave a committed mutation with nothing // truthful to report -- see `runPostCommitTail`'s void overload for the @@ -711,7 +712,7 @@ GetBoardResult BoardModel::execute(const CreateTask& action) { transaction.Commit(); - // ── post-commit tail (morph#751) ──────────────────────────────── + // ── post-commit tail ──────────────────────────────────────────── // The row is durable from the line above; `logAction` is not, and // `_log->append`/`flush` can throw. A throw here must not tell the // caller the CreateTask failed. @@ -757,7 +758,7 @@ GetBoardResult BoardModel::execute(const AddComment& action) { event.createdAtMs = nowMs(); mapper->Create(event); - // Built before the commit, not after it (morph#751). The board state + // Built before the commit, not after it. The board state // is this call's whole return value, so a re-read that fails must roll // the write back rather than leave a committed mutation with nothing // truthful to report -- see `runPostCommitTail`'s void overload for the @@ -766,7 +767,7 @@ GetBoardResult BoardModel::execute(const AddComment& action) { transaction.Commit(); - // ── post-commit tail (morph#751) ──────────────────────────────── + // ── post-commit tail ──────────────────────────────────────────── // The row is durable from the line above; `logAction` is not, and // `_log->append`/`flush` can throw. A throw here must not tell the // caller the AddComment failed. @@ -822,7 +823,7 @@ Ack BoardModel::execute(const AddAttachment& action) { transaction.Commit(); - // ── post-commit tail (morph#751) ──────────────────────────────── + // ── post-commit tail ──────────────────────────────────────────── // Nothing here is read back for the caller -- the answer is `Ack{}`, // which is already true the moment the commit above returns. All the // tail does is journal, and a journal that refuses the entry does not @@ -903,7 +904,7 @@ Ack BoardModel::execute(const RemoveAttachment& action) { mapper->Delete(rows.front()); transaction.Commit(); - // ── post-commit tail (morph#751) ──────────────────────────────── + // ── post-commit tail ──────────────────────────────────────────── runPostCommitTail([&] { logAction(action, Ack{}); }, "RemoveAttachment"); return Ack{}; } catch (...) { @@ -929,7 +930,7 @@ CreateRuleResult BoardModel::execute(const CreateRule& action) { const auto projectDbId = static_cast(std::stoull(*_projectIdStr)); // The rule is created on the board this handler is attached to, so an // action naming a different project is refused rather than written - // onto the attached board under another project's name (morph#369). + // onto the attached board under another project's name. requireProjectMatchesAttachedBoard(action.projectId, projectDbId); auto project = loadProjectById(mapper.Get(), projectDbId); @@ -959,12 +960,12 @@ CreateRuleResult BoardModel::execute(const CreateRule& action) { mapper->Create(rec); // Read before the commit, for the same reason `CreateColumn` builds - // its state there (morph#751): `rec.id` is the caller's whole answer. + // its state there: `rec.id` is the caller's whole answer. const CreateRuleResult result{.ruleId = RuleId{static_cast(rec.id.Value())}}; transaction.Commit(); - // ── post-commit tail (morph#751) ──────────────────────────────── + // ── post-commit tail ──────────────────────────────────────────── runPostCommitTail([&] { logAction(action, result); }, "CreateRule"); return result; } catch (...) { @@ -985,7 +986,7 @@ GetRulesResult BoardModel::execute(const GetRules& action) { const auto projectDbId = static_cast(std::stoull(*_projectIdStr)); // The rules listed are the attached board's, so an action naming a // different project is refused rather than answered with this board's - // rules under the other project's name (morph#369). + // rules under the other project's name. requireProjectMatchesAttachedBoard(action.projectId, projectDbId); // loadProjectById's only purpose here is the same NotFound-if-attached- // project-was-deleted check every other read in this file makes; its @@ -1040,7 +1041,7 @@ Ack BoardModel::execute(const DeleteRule& action) { mapper->Delete(rows.front()); transaction.Commit(); - // ── post-commit tail (morph#751) ──────────────────────────────── + // ── post-commit tail ──────────────────────────────────────────── runPostCommitTail([&] { logAction(action, Ack{}); }, "DeleteRule"); return Ack{}; } catch (...) { @@ -1071,7 +1072,7 @@ ApplyTagMutationResult BoardModel::execute(const ApplyTagMutation& action) { // (once here unconditionally, once again with the causal link), which // `morph::journal::replay()` would then dispatch twice, breaking the // "exactly once" invariant design spec §9 requires of a cascade. - // ── post-commit tail (morph#751) ──────────────────────────────── + // ── post-commit tail ──────────────────────────────────────────── // `applyTagMutationImpl` above owns the transaction and has already // committed by the time it returns, so this `logAction` is post-commit // exactly as the other handlers' are, even though the `Commit()` is not @@ -1323,7 +1324,7 @@ GetBoardResult BoardModel::execute(const MoveTaskPosition& action) { transaction.Commit(); - // ── post-commit tail (morph#566) ──────────────────────────────── + // ── post-commit tail ──────────────────────────────────────────── // The move is durable from the line above. Everything below -- // journalling, the rule cascade, and the post-cascade re-read -- is // follow-on work, and none of it can un-apply what was just committed. diff --git a/examples/kanban/tests/test_board_dto.cpp b/examples/kanban/tests/test_board_dto.cpp index abf351549..a9dc5206a 100644 --- a/examples/kanban/tests/test_board_dto.cpp +++ b/examples/kanban/tests/test_board_dto.cpp @@ -61,9 +61,9 @@ TEST_CASE("AddComment requires an engaged taskId and non-empty body", "[kanban][ TEST_CASE("kanban::BoardEventId: fromRowId rejects the one value it cannot represent", "[kanban][types]") { // 0 is BoardEventId's "not entered" sentinel, so an event id of 0 would - // arrive as *absent* and a real event would read as "no event" - // (morph#215). Row ids start at 1, so this never fires in practice -- it - // turns a silent collapse into a loud failure at the boundary. + // arrive as *absent* and a real event would read as "no event". Row ids + // start at 1, so this never fires in practice -- it turns a silent + // collapse into a loud failure at the boundary. CHECK_THROWS_AS(kanban::BoardEventId::fromRowId(0), kanban::KanbanError); } diff --git a/examples/kanban/tests/test_board_model.cpp b/examples/kanban/tests/test_board_model.cpp index e8efefb5a..da5b02b88 100644 --- a/examples/kanban/tests/test_board_model.cpp +++ b/examples/kanban/tests/test_board_model.cpp @@ -549,10 +549,9 @@ TEST_CASE("MoveTaskPosition rejects a taskId that belongs to a different project REQUIRE(found != stateB.tasks.end()); } -// Ledger triage item #14: the swimlane-belongs-to-project check -// (MoveTaskPosition's inline check next to requireColumnBelongsToProject) -// had no dedicated unit test -- only ever exercised implicitly by every -// other test supplying a real swimlane. Same shape as "MoveTaskPosition +// The swimlane-belongs-to-project check (MoveTaskPosition's inline check next +// to requireColumnBelongsToProject) is otherwise only exercised implicitly, by +// every other test supplying a real swimlane. Same shape as "MoveTaskPosition // into a column deleted mid-drag throws NotFound" above, but for the // swimlane half of the destination. TEST_CASE("MoveTaskPosition into a swimlane deleted mid-drag throws NotFound, not a silent orphan write", @@ -827,13 +826,13 @@ TEST_CASE("Replaying a move-to-Done journal entry does not re-fire its rule", "[ CHECK(closedTagCount == 1); } -// morph#369: `CreateRule`/`GetRules` each carry a `projectId` their own -// `validate()` insists be present. Neither used to consult it -- the board -// was named by the handler's attach state alone -- so a handler attached to -// project A, asked for project B's rules, answered with A's and an `ok`. The -// argument was load-bearing in the type and inert in the code, and a client -// that passed the id it meant had no way to tell it had been ignored. Both -// now refuse a mismatch the way every other cross-project check in +// `CreateRule`/`GetRules` each carry a `projectId` their own `validate()` +// insists be present, and each must consult it. If the board were named by the +// handler's attach state alone, a handler attached to project A and asked for +// project B's rules would answer with A's and an `ok`: the argument would be +// load-bearing in the type and inert in the code, and a client that passed the +// id it meant would have no way to tell it had been ignored. Both refuse a +// mismatch the way every other cross-project check in // board_model.cpp does. alice is a Manager on *both* projects here, so what // these pin is the projectId check itself, not the role gate standing in // for it. @@ -1043,7 +1042,7 @@ TEST_CASE("ActionKeyTraits::key() rejects a disengaged projectId inst CHECK_THROWS_AS(morph::model::ActionKeyTraits::key(disengaged), std::runtime_error); // The refusal is now `morph::model::keyToString`'s own, not this rung's: - // BRIDGE_MODEL_KEY generates the specialisation (morph#183), so the + // BRIDGE_MODEL_KEY generates the specialisation, so the // hand-written `throw kanban::ValidationError` is gone and the escaping // type is morph's plain `std::runtime_error`. Pinned as a negative // because it is the one caller-visible difference the migration makes. @@ -1062,28 +1061,27 @@ TEST_CASE("ActionKeyTraits::key() rejects a disengaged projectId inst CHECK_FALSE(refusedByTheRung); } -// Equivalence pin for morph#183's migration, and a regression guard after it: -// the two hand-written specialisations were replaced by one -// `BRIDGE_MODEL_KEY(BoardModel, OpenBoard, &OpenBoard::projectId)`, and the -// only way that is safe is if the generated key is the same bytes the -// hand-written `key()` produced, for every id it accepted. +// Equivalence pin: one +// `BRIDGE_MODEL_KEY(BoardModel, OpenBoard, &OpenBoard::projectId)` stands in for +// two hand-written specialisations, and the only way that is safe is if the +// generated key is the same bytes a hand-written `key()` produces, for every id +// it accepts. TEST_CASE("BoardModel's deduced key is the ProjectId itself and encodes exactly as the hand-written key() did", "[kanban][model][key]") { // The macro deduces `PrimaryKey` from the *member* it is handed, so the // model's key type is the strong id (examples/IMPLEMENTATION.md rule 3), - // not the unwrapped `std::int64_t` the hand-written - // `ModelKeyTraits` declared. Checked at run time rather than - // with `STATIC_REQUIRE` deliberately: this way the assertion *fails* - // against the pre-migration header instead of refusing to compile it, so - // it is a test that can be seen to fail. + // not the unwrapped `std::int64_t` a hand-written + // `ModelKeyTraits` would declare. Checked at run time rather + // than with `STATIC_REQUIRE` deliberately: this way the assertion *fails* + // against a header that declares the scalar instead of refusing to compile + // it, so it is a test that can be seen to fail. CHECK((std::same_as, kanban::ProjectId>)); - // The right-hand side is literally the body the hand-written - // specialisation had (`morph::model::keyToString(*action.projectId)`), so - // this compares generated against hand-written directly rather than - // against a re-derived expectation. `9007199254740993` is past 2^53 -- - // the range morph#286 had to fix elsewhere in this rung -- so a key - // encoding that ever went through a double would show up here. + // The right-hand side is literally a hand-written specialisation's body + // (`morph::model::keyToString(*action.projectId)`), so this compares + // generated against hand-written directly rather than against a re-derived + // expectation. `9007199254740993` is past 2^53, so a key encoding that ever + // went through a double would show up here. for (const std::int64_t raw : {std::int64_t{1}, std::int64_t{7}, std::int64_t{4294967297}, std::int64_t{9007199254740993}}) { const kanban::OpenBoard action{.projectId = kanban::ProjectId{raw}}; diff --git a/examples/kanban/tests/test_board_model_journal_outcome.cpp b/examples/kanban/tests/test_board_model_journal_outcome.cpp index c6af1d988..1a64b17f2 100644 --- a/examples/kanban/tests/test_board_model_journal_outcome.cpp +++ b/examples/kanban/tests/test_board_model_journal_outcome.cpp @@ -17,12 +17,12 @@ // These cases pin that a refused mutating action now leaves exactly that // entry. // -// morph#757 widened that from the four domain refusals to *any* pre-commit -// failure. Until then every handler ended `catch (const KanbanError&)`, so -// `Outcome::Failed` was written for this rung's own refusals and for nothing -// else -- an `std::invalid_argument` out of `std::stoull`, one of -// Lightweight's SQL exceptions from a pre-commit query, or a `std::bad_alloc` -// all reached the caller having journalled nothing at all. Nothing in +// It is *any* pre-commit failure, not only the four domain refusals. A handler +// ending `catch (const KanbanError&)` would write `Outcome::Failed` for this +// rung's own refusals and for nothing else -- an `std::invalid_argument` out of +// `std::stoull`, one of Lightweight's SQL exceptions from a pre-commit query, or +// a `std::bad_alloc` would all reach the caller having journalled nothing at +// all. Nothing in // "a rejected attempt is itself audit-worthy" depends on the exception's // type; only the `catch` clause did. The last three cases in this file cover // the widened path, including the failure mode widening it introduces: the @@ -139,9 +139,9 @@ enum class Ending : std::uint8_t { /// arrangement, never a pass. Returned, /// @brief It threw one of this rung's four `KanbanError` types -- the only - /// thing `Outcome::Failed` was written for before morph#757. + /// class a `catch (const KanbanError&)` handler would journal. DomainRefusal, - /// @brief It threw something else. This is the class morph#757 is about. + /// @brief It threw something else. This is the class these cases are about. NonDomainFailure, }; @@ -215,7 +215,7 @@ struct FailedEntry { /// `do { try { ... } catch (...) { ... } } while (... && ...)`, which /// `readability-function-cognitive-complexity` scores at 4. Six of them /// written out in the `TEST_CASE` below put it at 32 against a threshold of -/// 25 and failed `clang-tidy-diff` on morph#768. The assertions are the same +/// 25, which fails `clang-tidy-diff`. The assertions are the same /// assertions and there are the same number of them -- they are reported /// against this function's lines instead of the caller's, which is the whole /// of the difference. @@ -290,7 +290,7 @@ TEST_CASE("A ValidationError refusal on CreateColumn leaves a Failed journal ent CHECK_FALSE(entries.front().error.empty()); } -// ── morph#757: the failure that journalled nothing ─────────────────────────── +// ── The failure a KanbanError-only catch would journal nothing for ─────────── TEST_CASE("A non-KanbanError pre-commit failure leaves a Failed journal entry", "[kanban][journal][morph#757]") { DbFixture fixture; @@ -302,12 +302,12 @@ TEST_CASE("A non-KanbanError pre-commit failure leaves a Failed journal entry", auto log = std::make_shared(); model.attachActionLog(log, std::to_string(*projectId)); - // The failure morph#566 observed in CI on this rung, arranged rather than + // The failure CI's parallelism produces on this rung, arranged rather than // waited for: a real second connection holding a real write lock, so the // handler's own pre-commit statement takes a genuine `SQLITE_BUSY` past a // genuine (shortened) busy timeout. Not a mock and not a fault-injection - // seam in the model -- the handler cannot tell this apart from the - // contention that produced the original report. + // seam in the model -- the handler cannot tell this apart from real + // contention. const ScopedShortBusyTimeout shortTimeout{200}; auto drained = drainPoolIdleMappers(); const DbBusyFixture busy{"board_columns"}; @@ -315,7 +315,7 @@ TEST_CASE("A non-KanbanError pre-commit failure leaves a Failed journal entry", // Not REQUIRE_THROWS_AS: the point of the case is the exception's type, // so "it was not a KanbanError" has to be asserted rather than assumed // from the name of whatever Lightweight throws. A `DomainRefusal` here - // would mean the case was testing nothing morph#757 changed. + // would mean the case was exercising the arm every handler already had. REQUIRE(classify([&] { (void)model.execute(kanban::CreateColumn{.name = "To Do", .wipLimit = 0}); }) == Ending::NonDomainFailure); @@ -323,13 +323,12 @@ TEST_CASE("A non-KanbanError pre-commit failure leaves a Failed journal entry", // drained mappers can go back (`drainPoolIdleMappers`'s own doc comment). drained.clear(); - // Before morph#757 there were zero entries here: the handler's catch - // named `KanbanError` and this exception is not one, so it left with the - // journal untouched. + // A handler whose catch named `KanbanError` would leave zero entries here, + // this exception not being one. const auto entries = log->entries(); - // The headline assertion of morph#757, kept in the case body rather than - // moved into the helper below: on the tree this ticket started from it - // reads `0 == 1`, and that number *is* the defect. + // The headline assertion, kept in the case body rather than moved into the + // helper below: with a narrower catch it reads `0 == 1`, and that number + // *is* the defect. REQUIRE(entries.size() == 1); checkFailedEntry(entries.front(), FailedEntry{.actionType = morph::model::ActionTraits::typeId(), @@ -354,8 +353,7 @@ TEST_CASE("A journal that throws on the failure path does not replace the refusa REQUIRE_THROWS_AS(model.execute(kanban::CreateColumn{.name = "", .wipLimit = 0}), kanban::ValidationError); // Without this the case would pass on a tree where the journal was never - // consulted on the failure path at all -- which is the tree morph#757 - // started from. + // consulted on the failure path at all. CHECK(log->appendAttempts >= 1); } @@ -374,8 +372,8 @@ TEST_CASE("A journal that throws on the failure path does not replace a non-doma auto drained = drainPoolIdleMappers(); const DbBusyFixture busy{"board_columns"}; - // The compounded case: a non-domain failure -- the one that only reaches - // the journal at all after morph#757 -- while the journal it now writes + // The compounded case: a non-domain failure -- the class a narrower catch + // would not journal at all -- while the journal it writes // to is itself failing. `ValidationError` above proves containment by the // exception's type; here there is no type to lean on, so the message is // what distinguishes the original failure from the logging one. diff --git a/examples/kanban/tests/test_board_post_commit_tail.cpp b/examples/kanban/tests/test_board_post_commit_tail.cpp index fa7201c81..16b3b93a3 100644 --- a/examples/kanban/tests/test_board_post_commit_tail.cpp +++ b/examples/kanban/tests/test_board_post_commit_tail.cpp @@ -1,20 +1,19 @@ // SPDX-License-Identifier: Apache-2.0 // -// morph#566: a `MoveTaskPosition` whose *post-commit* work throws must not -// report the move as failed -- the row it wrote is already committed. +// A `MoveTaskPosition` whose *post-commit* work throws must not report the move +// as failed -- the row it wrote is already committed. // -// The CI observation this pins was a board whose `execute(MoveTaskPosition)` -// threw and whose move was nonetheless observed applied -// (`test_kanban_offline.cpp:672`, `movedCount == 1` under 32-way contention). -// That needs no race inside the commit path: `execute()` commits, then runs -// `logAction`, `evaluateRules` and a final `buildState` -- every one of which -// can throw -- and before this fix nothing between them and the caller -// distinguished "the move did not happen" from "the move happened and the -// follow-on work did not". +// The symptom is a board whose `execute(MoveTaskPosition)` threw and whose move +// was nonetheless observed applied (`test_kanban_offline.cpp:672`, +// `movedCount == 1` under 32-way contention). That needs no race inside the +// commit path: `execute()` commits, then runs `logAction`, `evaluateRules` and a +// final `buildState` -- every one of which can throw -- and without the shield +// nothing between them and the caller distinguishes "the move did not happen" +// from "the move happened and the follow-on work did not". // -// The contended `SQLITE_BUSY` that produced it in CI is not reproducible on -// demand. The *shape* is, deterministically: make the first step of the tail -// (`logAction`) throw, and assert the caller is told the truth. +// The contended `SQLITE_BUSY` that produces it under CI's parallelism is not +// reproducible on demand. The *shape* is, deterministically: make the first step +// of the tail (`logAction`) throw, and assert the caller is told the truth. #include #include @@ -135,31 +134,30 @@ TEST_CASE("MoveTaskPosition reports success when only its post-commit tail fails CHECK(columnOfTask(model, task) == columnB); } -// ── morph#751: the other nine handlers ─────────────────────────────────────── +// ── The other nine handlers ────────────────────────────────────────────────── // -// The case above covers `MoveTaskPosition`, the one handler morph#566 reached. -// The audit morph#751 asked for classified all sixteen `BoardModel::execute` -// overloads: +// The case above covers `MoveTaskPosition`. All sixteen `BoardModel::execute` +// overloads classify as: // // * six are read-only and open no transaction at all -- `OpenBoard`, // `GetBoardState`, `GetAttachments`, `GetRules`, `GetEventsSince`, // `GetActivity`. Nothing to shield; wrapping them would add a branch no // input can take. -// * one was already shielded -- `MoveTaskPosition`, above. +// * one is `MoveTaskPosition`, above. // * **nine commit and then keep working**, and every one of those nine runs // `logAction` after its `Commit()`. Four of them (`CreateColumn`, // `CreateSwimlane`, `CreateTask`, `AddComment`) also re-read the board -// with `buildState` for their return value; those reads moved *inside* the +// with `buildState` for their return value; those reads sit *inside* the // transaction rather than being shielded, so a failed re-read rolls the // write back instead of leaving a committed mutation with nothing truthful // to report. What is left after the commit is `logAction` alone, and that -// is what `runPostCommitTail` now contains at all nine. +// is what `runPostCommitTail` contains at all nine. // // `ApplyTagMutation` is the one whose commit is not visible in the handler -- // `applyTagMutationImpl` owns the transaction -- but its `logAction` is // post-commit all the same, which is why it is in the list. // -// Each case below asserts the same two things morph#566's does: the call does +// Each case below asserts the same two things the case above does: the call does // not throw, and `appendAttempts` proves the tail genuinely ran and genuinely // failed. Without the second, every one of these would pass on a tree where // `runPostCommitTail` had been deleted and the log never consulted. diff --git a/examples/kanban/tests/test_board_presenter.cpp b/examples/kanban/tests/test_board_presenter.cpp index bf5eb39c7..7fdcceb4d 100644 --- a/examples/kanban/tests/test_board_presenter.cpp +++ b/examples/kanban/tests/test_board_presenter.cpp @@ -113,7 +113,7 @@ TEST_CASE( "BoardPresenter::openBoard, dispatched the instant the presenter is constructed over " "a stalled Socket-mode attach, still reports the board once the attach reply lands", "[kanban][gui][presenter]") { - // morph#305's premise: `openBoard()` fired immediately after construction + // The premise worth pinning: `openBoard()` fired immediately after construction // races the handler's registration and can fail fast with "handler not // bound". `OpenBoard` is payload-keyed (BRIDGE_MODEL_KEY, board_model.hpp) // so `BridgeHandler::execute()` routes it through `Bridge::attachHandlerAsync` diff --git a/examples/kanban/tests/test_board_qml_bridge.cpp b/examples/kanban/tests/test_board_qml_bridge.cpp index 31e3305dc..bfae78f4a 100644 --- a/examples/kanban/tests/test_board_qml_bridge.cpp +++ b/examples/kanban/tests/test_board_qml_bridge.cpp @@ -383,8 +383,8 @@ TEST_CASE("BoardBridge::submitIfValid runs the board forms and keeps the board p TEST_CASE("BoardBridge::submitIfValid submits CreateRule and re-lists rules", "[kanban][gui][qml-bridge][issue344][issue393]") { - // CreateRule is the fifth form (morph#393) and the only one that does not - // return the rebuilt board state -- BoardPresenter::submitForm decodes + // CreateRule is the fifth form and the only one that does not return the + // rebuilt board state -- BoardPresenter::submitForm decodes // CreateRuleResult on its own branch and re-emits ruleCreated instead of // boardOpened, exactly as the typed createRule() call does. DbFixture fixture; diff --git a/examples/kanban/tests/test_board_unattached.cpp b/examples/kanban/tests/test_board_unattached.cpp index b0000c1a4..791528cac 100644 --- a/examples/kanban/tests/test_board_unattached.cpp +++ b/examples/kanban/tests/test_board_unattached.cpp @@ -25,7 +25,7 @@ // cases enter that state deliberately: `attachActionLog(log, {})` is character // for character the call `ModelFactory::create` makes. // -// Filed as morph#368; the out-of-process counterpart is +// The out-of-process counterpart is // `scripts/scenario/scenarios/kanban/a-board-must-be-opened-before-it-answers.scenario`. #include @@ -104,9 +104,9 @@ class UnattachedBoard { } // namespace -// The two guards issue #368 singles out: they are written, they are correct, -// and over a server they never fired. Split into their own cases so a -// regression in either is named by the failing test rather than by an +// The two guards a server reaches first: they are written, they are correct, +// and it is only over a server that they are ever asked. Split into their own +// cases so a regression in either is named by the failing test rather than by an // assertion line. TEST_CASE("GetActivity on an unattached handler names the action and OpenBoard", "[kanban][model][unattached]") { DbFixture fixture; @@ -114,8 +114,8 @@ TEST_CASE("GetActivity on an unattached handler names the action and OpenBoard", const ScopedPrincipal alice{"alice"}; // Both halves matter and they are separate claims: the *type* is what a - // caller's `.onError(...)` branches on, and it was `std::invalid_argument` - // -- outside kanban's hierarchy altogether -- while the guard was dead. + // caller's `.onError(...)` branches on, and with the guard fallen through it + // is `std::invalid_argument` -- outside kanban's hierarchy altogether. CHECK_THROWS_AS(board.get().execute(kanban::GetActivity{}), kanban::NotFound); CHECK(errorTextOf([&] { return board.get().execute(kanban::GetActivity{}); }) == "GetActivity: handler was never attached via OpenBoard"); @@ -134,9 +134,9 @@ TEST_CASE("GetRules on an unattached handler names the action and OpenBoard", "[ "GetRules: handler was never attached via OpenBoard"); } -// All fifteen guards, in one case: the nine actions issue #368 lists -- every -// one of which answered the bare string "stoull" over a server -- and the six -// it does not. +// All fifteen guards, in one case: the nine actions the scenario drives -- each +// of which answers the bare string "stoull" over a server if its guard falls +// through -- and the six it does not. TEST_CASE("Every action on an unattached handler is refused by name, never with \"stoull\"", "[kanban][model][unattached]") { DbFixture fixture; @@ -176,10 +176,9 @@ TEST_CASE("Every action on an unattached handler is refused by name, never with CHECK(errorTextOf([&] { return model.execute(kanban::GetRules{.projectId = projectId}); }) == "GetRules: handler was never attached via OpenBoard"); - // The rest of the fifteen. Not in issue #368's list and not driven by the - // scenario either -- which is exactly why they are here: a regression that - // re-opened the fall-through in one of these six would otherwise pass - // every file written to prevent it. + // The rest of the fifteen. Not driven by the scenario -- which is exactly + // why they are here: a regression that re-opened the fall-through in one of + // these six would otherwise pass every file written to prevent it. CHECK(errorTextOf([&] { return model.execute(kanban::AddAttachment{.taskId = kanban::TaskId{1}, .filename = "spec.pdf", @@ -316,8 +315,8 @@ TEST_CASE("Attaching a log with an empty entityKey does not un-attach an open bo const auto state = model.execute(kanban::GetBoardState{}); CHECK(state.projectId == projectId); - // #422's own version of this hazard: an empty entityKey must not pull - // _entityKeyStr away from the board _projectIdStr still names either -- + // The second-attach version of this hazard: an empty entityKey must not pull + // _entityKeyStr away from the board _projectIdStr still names -- // otherwise every entry logged after this second attach call would carry // entityKey="" while execute(GetActivity) keeps reading entries by // *_projectIdStr, making them invisible to it despite the handler still @@ -329,14 +328,14 @@ TEST_CASE("Attaching a log with an empty entityKey does not un-attach an open bo CHECK(activity.events.front().actionType == "CreateColumn"); } -// #422's own residue: a `contextKey` the attach guard rejects used to produce -// two different journal entity keys for the *same* attach call -- the raw -// key from a holder-wrapped instance's `_contextKey` (`IModelHolder`, -// `morph/core/model.hpp`, set unconditionally), and the empty string from -// this `BoardModel`'s own `_projectIdStr`, which the guard had correctly -// left disengaged. `_entityKeyStr` closes that gap by taking the same -// unconditional assignment `_contextKey` does, so the two now agree even -// though the attach guard still refuses "foo" as a board. +// A `contextKey` the attach guard rejects must still produce one journal entity +// key, not two, for the *same* attach call. Without a separate `_entityKeyStr` +// there are two: the raw key from a holder-wrapped instance's `_contextKey` +// (`IModelHolder`, `morph/core/model.hpp`, set unconditionally), and the empty +// string from this `BoardModel`'s own `_projectIdStr`, which the guard correctly +// leaves disengaged. `_entityKeyStr` takes the same unconditional assignment +// `_contextKey` does, so the two agree even though the attach guard still +// refuses "foo" as a board. TEST_CASE("A rejected contextKey still produces the raw key as the journal entityKey, matching IModelHolder", "[kanban][model][unattached]") { DbFixture fixture; @@ -349,13 +348,14 @@ TEST_CASE("A rejected contextKey still produces the raw key as the journal entit // CreateColumn, not GetBoardState: GetBoardState is Loggable::No and its // execute() has no try/catch at all, so it never reaches logFailure. // CreateColumn's own attach guard throw is caught by its execute()'s - // catch-all handler, which does. The attach guard still refuses "foo" - // as a board either way -- this is the existing #368 behavior, unaffected - // by #422's split. + // catch-all handler, which does. The attach guard refuses "foo" as a board + // either way; that is the unattached-handler behaviour above, which the + // separate `_entityKeyStr` does not change. CHECK_THROWS_AS(model.execute(kanban::CreateColumn{.name = "To Do", .wipLimit = 0}), kanban::NotFound); - // CreateColumn's own catch block already journalled the refusal before - // rethrowing -- exactly the path that used to stamp "" instead of "foo". + // CreateColumn's own catch block journals the refusal before rethrowing -- + // the path that would stamp "" instead of "foo" if `_entityKeyStr` were + // `_projectIdStr`. const auto entries = log->entries(); REQUIRE(entries.size() == 1); CHECK(entries.front().entityKey == "foo"); diff --git a/examples/kanban/tests/test_gui_forms_render.cpp b/examples/kanban/tests/test_gui_forms_render.cpp index f2664cde0..80779942e 100644 --- a/examples/kanban/tests/test_gui_forms_render.cpp +++ b/examples/kanban/tests/test_gui_forms_render.cpp @@ -13,10 +13,9 @@ // `DynamicForm` bound to a real bridge produced the fields the schema // describes, that the two *hidden* context fields (`CreateTask`'s // columnId/swimlaneId, `AddComment`'s taskId) are actually engaged by the view -// that owns them, or that a submit reaches the model. Those are exactly the -// claims morph#344 turns on -- "schema generation working is necessary, not -// sufficient; nothing has been rendered on screen" -- so they get a test that -// renders on screen. +// that owns them, or that a submit reaches the model. Schema generation working +// is necessary and not sufficient -- nothing has been rendered on screen -- so +// those claims get a test that renders on screen. // // It stays inside `ladder_kanban_tests` rather than becoming a third binary // (bank's `bank_gui_qml_tests` is the ladder's precedent for that shape): this @@ -191,9 +190,10 @@ void pressSubmit(QObject* form) { /// records that adding one with nothing to call it would be a stub. So the /// engine warns once per form, for every conforming controller in the ladder, /// the moment a *real* controller is attached. The rule-6 smoke test never sees -/// it because it attaches none. Filed as morph#387; tolerated by exact text -/// here rather than by dropping the assertion, so any *other* warning — a -/// misspelled handler, a missing property, a broken binding — still fails. +/// it because it attaches none. It is filed against the renderer; tolerated +/// here by exact text rather than by dropping the assertion, so any *other* +/// warning — a misspelled handler, a missing property, a broken binding — still +/// fails. /// @param engine The engine to load into (kept alive by the caller). /// @param typeName Unqualified QML type name within `MORPH_LADDER_QML_URI`. /// @param properties Initial properties for the root object. @@ -205,7 +205,7 @@ void pressSubmit(QObject* form) { const QString text = warning.toString(); if (text.contains(QStringLiteral("onOptionsReceived")) && text.contains(QStringLiteral("MorphForms/qml/DynamicForm.qml"))) { - continue; // morph#387 — see this function's doc comment. + continue; // The renderer's own warning — see this function's doc comment. } unexpected.append(text); } @@ -277,8 +277,8 @@ TEST_CASE("MembersView renders SetMemberRole through the shipped renderer and su // Loaded directly (not via ProjectListView's "Members" button, which this // rule-6 file cannot click): MembersView.qml's own projectAdminBridge/ // projectId initial properties are exactly what ProjectListView.qml wires - // into it, and SetMemberRole::role is the closed-set field morph#386 used - // to force to a free-text field. + // into it, and SetMemberRole::role is a closed-set field, which a renderer + // without enum support would draw as free text. DbFixture fixture; auto rig = makeAuthedRig("alice"); const qlonglong projectId = seedProject(*rig); @@ -297,7 +297,7 @@ TEST_CASE("MembersView renders SetMemberRole through the shipped renderer and su // `role` is a closed `oneOf`-of-`const`s (Role's glz::meta/glz::enumerate) // -- the renderer draws it as a ComboBox with the three named rows, not - // a free-text field, which is exactly the gap morph#386 closed. + // a free-text field. QObject* roleControl = control(form, QStringLiteral("field_role")); REQUIRE(roleControl != nullptr); const QVariantList roleOptions = form->property("fields").toList(); @@ -327,7 +327,7 @@ TEST_CASE("MembersView renders SetMemberRole through the shipped renderer and su // currentIndex -1 -- "no selection" -- so the gate needs `role` engaged // too, not just `principal` (DynamicForm.qml's resetFields()/currentIndex // comments). Membership is decidable client-side once the schema states - // the closed set (morph#386): an out-of-set value here would leave the + // the closed set: an out-of-set value here would leave the // field's own JSON literal null and the gate unsatisfied, which is a // stronger property than the free-text field this form replaced ever had. CHECK_FALSE(isReady(form)); @@ -467,7 +467,7 @@ TEST_CASE("BoardView renders CreateColumn/CreateSwimlane/CreateTask through the // Both ids ride out as JSON *numbers*, not quoted strings: the strong-id // `$ref` into `$defs` resolved to `{"type":["integer","null"]}` and the // renderer typed the field from it. A quoted id here is what the server - // rejects with parse_number_failure (morph#189's shape). + // rejects with parse_number_failure. CHECK(bodyOf(taskForm) == QStringLiteral(R"({"columnId":%1,"swimlaneId":%2,"title":"Fix bug"})").arg(columnId, swimlaneId)); @@ -489,9 +489,9 @@ TEST_CASE("RulesView renders CreateRule through the shipped renderer and submits // Loaded directly (not via BoardView's "Rules" popup button, which this // rule-6 file cannot click): RulesView.qml's own boardBridge initial // property is exactly what BoardView.qml wires into it. - // `CreateRule::mutationType` is the closed-set field morph#386 used to - // force to a free-text field; `triggerColumnId` is the Choice field this - // rung's first server-fetched combo box (morph#393). + // `CreateRule::mutationType` is a closed-set field, which a renderer without + // enum support would draw as free text; `triggerColumnId` is the Choice + // field behind this rung's first server-fetched combo box. DbFixture fixture; auto rig = makeAuthedRig("alice"); const qlonglong projectId = seedProject(*rig); @@ -518,8 +518,7 @@ TEST_CASE("RulesView renders CreateRule through the shipped renderer and submits CHECK(form->property("actionType").toString() == QStringLiteral("CreateRule")); // `mutationType` is a closed `oneOf`-of-`const`s (RuleMutationType's - // glz::meta/glz::enumerate) -- a ComboBox, not the free-text field - // morph#386 used to force. + // glz::meta/glz::enumerate) -- a ComboBox, not a free-text field. REQUIRE(control(form, QStringLiteral("field_mutationType")) != nullptr); // `triggerColumnId` is a Choice (`x-optionsAction: "GetBoardState"`), so diff --git a/examples/kanban/tests/test_kanban_offline.cpp b/examples/kanban/tests/test_kanban_offline.cpp index bc581987a..95cb39d9b 100644 --- a/examples/kanban/tests/test_kanban_offline.cpp +++ b/examples/kanban/tests/test_kanban_offline.cpp @@ -158,7 +158,7 @@ TEST_CASE("Dropping MoveTaskPosition's reply frame and retrying is exactly-once, // be destroyed *before* the Bridge, and the handler would write into dead // stack slots: AddressSanitizer reports precisely that as a // stack-use-after-scope (caught by the ladder's ASan+UBSan leg). Same - // hazard, and same cause, as morph#137 -- a callback outliving the frame + // hazard, and same cause, as any callback outliving the frame // it captured by reference. bool firstResolved = false; bool firstFailed = false; diff --git a/examples/kanban/tests/test_kanban_qml_surface.cpp b/examples/kanban/tests/test_kanban_qml_surface.cpp index d948e3cce..07d48a987 100644 --- a/examples/kanban/tests/test_kanban_qml_surface.cpp +++ b/examples/kanban/tests/test_kanban_qml_surface.cpp @@ -77,29 +77,27 @@ TEST_CASE("Every kanban bridge exposes exactly the surface gui/qml binds, and no audit.bind(QStringLiteral("projectAdminBridge"), projectAdminBridge); // ── What is exempt, and why each one is permanent ──────────────────── - // The first run of this audit reported nine members BoardBridge publishes - // that no file under gui/qml/ binds, recorded as a backlog under - // morph#291. That backlog is now worked off, and nothing here is "tracked, - // decide later": every one of the nine was dispositioned by binding or - // deleting the member, and the two that remain are exempt for a structural - // reason that will not change. + // Nine members BoardBridge publishes are the ones an unbound audit + // naturally reports. Nothing here is "tracked, decide later": each of the + // nine is dispositioned by binding or deleting the member, and the two that + // remain are exempt for a structural reason that will not change. // - // What went, and where it went: - // * `getRules`/`ruleCreated`/`ruleDeleted` -- these were not dead - // surface, they were the automation-rules pane never being populated - // at all (morph#304 §A2). BoardView.qml now fetches on `rulesPopup`'s - // `onOpened` and re-fetches on each mutation, so the audit resolves - // all three against real binding sites. - // * `attachmentUploaded`/`attachmentDownloaded` -- a missing control. - // TaskDetailPopup.qml now reports both outcomes; the download half in - // particular had no user-visible effect whatsoever, since it writes - // its bytes to a path outside the app. - // * `bound` -- genuinely dead, and deleted rather than exempted. See - // board_qml_bridge.hpp's own note where the signal used to be. - // * `queueDepth` -- genuinely unbound, and bound rather than deleted - // (morph#308). BoardView.qml now reads it for a pending-sync - // indicator beside the dead-letter banner, so the audit resolves it - // against a real binding site too. + // Where each of the nine went: + // * `getRules`/`ruleCreated`/`ruleDeleted` -- not dead surface. Unbound, + // they are the automation-rules pane not being populated at all. + // BoardView.qml fetches on `rulesPopup`'s `onOpened` and re-fetches on + // each mutation, so the audit resolves all three against real binding + // sites. + // * `attachmentUploaded`/`attachmentDownloaded` -- without a control they + // are invisible, the download half especially, since it writes its + // bytes to a path outside the app. TaskDetailPopup.qml reports both + // outcomes. + // * `bound` -- genuinely dead, so deleted rather than exempted. See + // board_qml_bridge.hpp's own note in its place. + // * `queueDepth` -- genuinely unbound, and bound rather than deleted. + // BoardView.qml reads it for a pending-sync indicator beside the + // dead-letter banner, so the audit resolves it against a real binding + // site too. // // The list is checked in both directions: an exemption for a member that // has since been deleted, or one QML has since bound, fails this test @@ -135,7 +133,7 @@ TEST_CASE("Every kanban bridge exposes exactly the surface gui/qml binds, and no audit.allowUnbound(QStringLiteral("projectAdminBridge"), QStringLiteral("submitIfValid"), rendererCalled); audit.allowUnbound(QStringLiteral("boardBridge"), QStringLiteral("submitIfValid"), rendererCalled); // `fetchOptions`/`optionsReceived` are the same seam, one field kind over: - // `CreateRule::triggerColumnId` (morph#393) is this rung's first + // `CreateRule::triggerColumnId` is this rung's first // `morph::forms::Choice` field, and DynamicForm.qml's own // Component.onCompleted calls `controller.fetchOptions(...)` and its // `Connections { target: controller }` block declares `onOptionsReceived` diff --git a/examples/kanban/tests/test_kanban_stress.cpp b/examples/kanban/tests/test_kanban_stress.cpp index 2185edac6..ff26fd075 100644 --- a/examples/kanban/tests/test_kanban_stress.cpp +++ b/examples/kanban/tests/test_kanban_stress.cpp @@ -49,20 +49,19 @@ // `next()`'s designed usage. Nothing here needs the eagerly-materialized // schedule TESTING.md's description would imply. // -// **No Qt anywhere in this file (fixing morph#128)**: the original version of -// this test drove everything through `BackendRig{Mode::Local, ...}` and -// `awaitQt`/`pumpUntil` (examples/common/testkit/pump.hpp), which the CI -// job's own comment claimed involved "no Qt/GUI" -- a claim morph#128 proved -// false: `Mode::Local` unconditionally constructs a real `morph::qt:: -// QtExecutor` for client-facing callback delivery (backend_rig.hpp's own -// doc comment explains why: pool-thread callback delivery would race -// pumpUntil/awaitQt's unsynchronized reads otherwise), and every one of the -// 165 ThreadSanitizer warnings morph#128 catalogued bottoms out in genuine -// Qt-internal frames (QMetaObject::invokeMethod, QCallableObject, -// QObject::event) reached through that QtExecutor. Since a prebuilt, -// non-TSan-instrumented Qt package can't be seen through by ThreadSanitizer, -// those warnings are unusable evidence either way -- real bugs or false -// positives, TSan cannot tell from outside an instrumented Qt build. +// **No Qt anywhere in this file**, and this is the file where that costs +// something. Driving it through `BackendRig{Mode::Local, ...}` and +// `awaitQt`/`pumpUntil` (examples/common/testkit/pump.hpp) is not "no Qt/GUI": +// `Mode::Local` unconditionally constructs a real `morph::qt::QtExecutor` for +// client-facing callback delivery (backend_rig.hpp's own doc comment explains +// why: pool-thread callback delivery would race pumpUntil/awaitQt's +// unsynchronized reads otherwise). Run that way under ThreadSanitizer it +// produces 165 warnings, every one of which bottoms out in genuine Qt-internal +// frames (QMetaObject::invokeMethod, QCallableObject, QObject::event) reached +// through that QtExecutor. Since a prebuilt, non-TSan-instrumented Qt package +// can't be seen through by ThreadSanitizer, those warnings are unusable evidence +// either way -- real bugs or false positives, TSan cannot tell from outside an +// instrumented Qt build. // // This version drives `BoardModel` through a bare `morph::bridge::Bridge` // wrapping a `morph::backend::LocalBackend` directly (the exact pattern @@ -128,7 +127,7 @@ struct InlineExecutor : morph::exec::IExecutor { void post(std::function fn) override { fn(); } }; -// -- Why the two durations below are two types (morph#735) ------------------- +// -- Why the two durations below are two types ------------------- // // This `waitUntil` used to take `(Pred, milliseconds budget = 20000ms, // milliseconds step = 5ms)`: two adjacent, same-type, both-defaulted @@ -146,16 +145,16 @@ struct InlineExecutor : morph::exec::IExecutor { // every route back to the hazard. // // The same two types, with the same names and the same explicit constructors, -// are what `tests/test_support.hpp`'s framework `waitUntil` grew in morph#721 +// are what `tests/test_support.hpp`'s framework `waitUntil` takes // -- deliberately the same shape rather than a third one. They are redeclared // here rather than included because `examples/` does not, and should not, // reach into the framework's own test support: this file's target links // `morph::ladder_testkit`, not `morph_test_main`'s private headers. // -// A `NOLINT` was not an option: it would remove the *warning* and leave the -// hazard (morph#404), and morph#715 measured the other near miss -- widening -// one parameter's type to silence `bugprone-easily-swappable-parameters` while -// the transposition still compiles. +// A `NOLINT` is not an option: it would remove the *warning* and leave the +// hazard. Nor is widening one parameter's type to silence +// `bugprone-easily-swappable-parameters`, which leaves the transposition +// compiling. /// @brief `waitUntil`'s overall polling budget: the longest it may wait before /// giving up and returning `false`. @@ -194,19 +193,19 @@ inline constexpr std::chrono::milliseconds kDefaultWaitStep{5}; /// @brief Polls @p pred until it returns `true` or @p budget elapses, /// sleeping @p step between polls. Same shape as `morph::testing:: -/// waitUntil` (`tests/test_support.hpp`), which grew these same two -/// strong types in morph#721, minus the Qt event-loop pump -- nothing +/// waitUntil` (`tests/test_support.hpp`), which takes these same two +/// strong types, minus the Qt event-loop pump -- nothing /// here needs one, since no callback in this file is ever queued onto /// a Qt event loop in the first place. /// /// @p budget is scaled by `MORPH_LADDER_DEADLINE_MS` exactly as every /// `pumpUntil` deadline is, via `testkit/deadline.hpp` -- the Qt-free half of /// `pump.hpp`, split out precisely so this file can share the knob without -/// acquiring the `` include that morph#128 exists to keep -/// out of it. Scaling here rather than at the eight-odd call sites is +/// acquiring the `` include this file's whole Qt-free rule +/// exists to keep out. Scaling here rather than at the eight-odd call sites is /// deliberate: it is one place, and it reaches the explicit 90s budget below -/// too. Before this, every other wait in `examples/` honoured the env var and -/// the slowest test in the ladder was the only one that did not. +/// too. Without it the slowest test in the ladder would be the only wait in +/// `examples/` that did not honour the env var. /// /// @tparam Pred Predicate polled for completion. /// @param pred Polled until it returns `true`. @@ -239,8 +238,8 @@ concept WaitUntilCallableWith = requires(Args... args) { waitUntil(args...); }; /// @brief A stand-in predicate type for the assertions below. using ExampleWaitPred = bool (*)(); -// The acceptance test for morph#735, kept in the translation unit that owns -// the hazard so the build reddens if a later edit reintroduces it. +// The acceptance test for that compile error, kept in the translation unit that +// owns the hazard so the build reddens if a later edit reintroduces it. // // What must keep working -- every call site in this file relies on the // defaults, and one passes an explicit budget: @@ -299,11 +298,10 @@ static_assert(!WaitUntilCallableWith); // spawn, join and verification -- the shape `tests/.clang-tidy:132` describes // when it subtracts this check for the framework's own tests: it "measures a // whole TEST_CASE body". The example rungs' test configs subtract only the -// Catch2 chained-comparison finding (a gate removed on 2026-09-23 validates -// that one claim per file), so the suppression goes here rather than widening -// theirs. The finding is pre-existing; morph#750 only made it visible by -// editing a waitUntil call inside the body, which pulls the whole function -// into clang-tidy-diff's changed-line scope (morph#677). +// Catch2 chained-comparison finding, so the suppression goes here rather than +// widening theirs. The finding does not depend on any particular edit -- it is a +// property of the body -- but clang-tidy-diff only reports it once a changed +// line pulls the whole function into its scope. // NOLINTNEXTLINE(readability-function-cognitive-complexity) TEST_CASE("Concurrent MoveTaskPosition calls (N=4) never desync positions -- run under ThreadSanitizer", "[kanban][stress][tsan]") { diff --git a/examples/kanban/tests/test_project_admin_qml_bridge.cpp b/examples/kanban/tests/test_project_admin_qml_bridge.cpp index 2f2f9ceb7..f02d2f7a8 100644 --- a/examples/kanban/tests/test_project_admin_qml_bridge.cpp +++ b/examples/kanban/tests/test_project_admin_qml_bridge.cpp @@ -406,7 +406,7 @@ TEST_CASE("ProjectAdminBridge::submitIfValid creates a project and still emits p TEST_CASE("ProjectAdminBridge::submitIfValid sets a member role and still emits memberRoleSet", "[kanban][gui][qml-bridge][issue344][issue393]") { - // MembersView.qml's "add member" row is a DynamicForm now (morph#393): + // MembersView.qml's "add member" row is a DynamicForm, so // submitForm's SetMemberRole branch decodes a bare Ack and re-emits // memberRoleSet exactly as the typed setMemberRole() call does, so // whichever path submitted, ProjectListView.qml's existing From 7ea7f45a5fa4bcb28aa0076e62478f02edc02bd4 Mon Sep 17 00:00:00 2001 From: Yaraslau Tamashevich Date: Thu, 24 Sep 2026 03:13:30 +0200 Subject: [PATCH 06/10] comments(examples/crm): state the constraint, drop the citation crm's README, the offline outbox header and four test suites. The GCC 16 `-Warray-bounds` false-positive block in test_pipeline.cpp keeps every part that is evidence -- the diagnostic text, the devirtualization GCC performs to reach it, the measured 112 vs. 136 byte holder sizes, and the `== 16` bound that makes the suppression lapse rather than accumulate; only the ticket number left. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW --- examples/crm/README.md | 6 +++--- examples/crm/include/crm/dto/opportunity_dto.hpp | 2 +- examples/crm/include/crm/offline/field_outbox.hpp | 2 +- examples/crm/src/db/schema.cpp | 2 +- examples/crm/tests/test_contact_model.cpp | 2 +- examples/crm/tests/test_dynamic_logic.cpp | 2 +- examples/crm/tests/test_offline_sync.cpp | 4 ++-- examples/crm/tests/test_pipeline.cpp | 2 +- examples/crm/tests/test_quote_model.cpp | 2 +- examples/crm/tests/test_saved_view.cpp | 2 +- 10 files changed, 13 insertions(+), 13 deletions(-) diff --git a/examples/crm/README.md b/examples/crm/README.md index 15875aa21..d8665b8f3 100644 --- a/examples/crm/README.md +++ b/examples/crm/README.md @@ -56,7 +56,7 @@ live", and each anchor marks one point on it: **DocType** JSON defines schema, DB table, form UI, list view, and REST API; custom fields are rows merged into the Meta at load time; child tables put order lines inside an order form (maps to morph's - nested-aggregate schema recursion, #35). Submitted documents are + nested-aggregate schema recursion). Submitted documents are immutable + amendable — a natural fit for an append-only journal. Docs: - Runtime ceiling, for orientation only: @@ -258,7 +258,7 @@ schemas/layouts). Build order (each step is a usable milestone): the served schema. A round-5 correction here said EspoCRM's condition *trees* "cannot be adopted as-is" because `x-rules` had no `and`/`or`/`not` and combinators would have to be filed as a framework proposal. **The - combinators shipped** (morph#78): `And`, `Or` and `Not` with the + combinators shipped**: `And`, `Or` and `Not` with the `andOf`/`orOf`/`notOf` builders (`include/morph/forms/forms.hpp`), and the emitted vocabulary carries `and`/`or`/`not` alongside `engaged`, `notEngaged`, `equals`, `greater` and `less`. Condition trees nest to any @@ -645,7 +645,7 @@ itself, not by this build, and neither gates 7a or 7b. `codecov.yml` scores `examples/crm/{src,include/crm}/models/**` and says, of this rung's entry specifically, that it "does NOT yet carry a per-miss audit establishing which of the 148 uncovered lines are unreachable-by-design rather -than merely untested" (morph#412). Ledger's and lims's entries carry one; each +than merely untested". Ledger's and lims's entries carry one; each ends by naming two or three defensive guards no caller can reach. **crm's answer is different, and that is the finding.** Measured over the whole diff --git a/examples/crm/include/crm/dto/opportunity_dto.hpp b/examples/crm/include/crm/dto/opportunity_dto.hpp index 20cc7236f..8c257e3f7 100644 --- a/examples/crm/include/crm/dto/opportunity_dto.hpp +++ b/examples/crm/include/crm/dto/opportunity_dto.hpp @@ -42,7 +42,7 @@ struct OpportunityView { /// @brief README build order §7 ("Dynamic logic... conditional required /// encoded in the served schema"), using the shipped `requiredWhen` -/// rule (morph#78) rather than a hand-written check: once a rep +/// rule rather than a hand-written check: once a rep /// enters an expected close value for the deal, the schema itself /// demands a primary contact before the record can be saved — a /// deal with real money behind it needs someone to actually talk to. diff --git a/examples/crm/include/crm/offline/field_outbox.hpp b/examples/crm/include/crm/offline/field_outbox.hpp index cecf8ffb2..4c440585a 100644 --- a/examples/crm/include/crm/offline/field_outbox.hpp +++ b/examples/crm/include/crm/offline/field_outbox.hpp @@ -15,7 +15,7 @@ /// A field rep's **write path** for offline opportunity edits (README build /// order §8). Same shape, and same reasoning, as `lims::offline::FieldOutbox` /// (that rung's §7) — see that header's own doc comment for the full -/// argument (`examples/IMPLEMENTATION.md` rule 1's carve-out, morph#197): +/// argument (`examples/IMPLEMENTATION.md` rule 1's carve-out): /// the framework supplies no seam for "detect an offline `execute()` and /// queue instead", so the app decides that at the dispatch site, and this /// class is that decision's domain-shaped half for crm's own entity. diff --git a/examples/crm/src/db/schema.cpp b/examples/crm/src/db/schema.cpp index 922a147ea..d4696f688 100644 --- a/examples/crm/src/db/schema.cpp +++ b/examples/crm/src/db/schema.cpp @@ -8,7 +8,7 @@ namespace crm::db { void configure(const std::string& connectionString) { - // morph#740: nothing in Lightweight stops a pooled DataMapper from being + // Nothing in Lightweight stops a pooled DataMapper from being // returned with a transaction still open on it -- `DataMapperPool::Return` // does no transaction cleanup, and the cost lands on the next, unrelated // borrower as a 60s stall and a `database is locked` it did not cause. diff --git a/examples/crm/tests/test_contact_model.cpp b/examples/crm/tests/test_contact_model.cpp index e81aa332c..dcde9ea01 100644 --- a/examples/crm/tests/test_contact_model.cpp +++ b/examples/crm/tests/test_contact_model.cpp @@ -169,7 +169,7 @@ TEST_CASE("ListContactOptions serves {id, name} rows for the primary-contact Cho } // The sibling of `AccountModel`/`LeadModel`/`OpportunityModel`/`QuoteModel`'s -// own "journals its edits against the attached identity" case (morph#412). +// own "journals its edits against the attached identity" case. // `ContactModel::attachActionLog` was called by no test at all, so the claim // that this model journals what it writes was supported by no executed line -- // the state lims was in before its own audit trail turned out to be recording diff --git a/examples/crm/tests/test_dynamic_logic.cpp b/examples/crm/tests/test_dynamic_logic.cpp index 3ffee3fd9..512da5236 100644 --- a/examples/crm/tests/test_dynamic_logic.cpp +++ b/examples/crm/tests/test_dynamic_logic.cpp @@ -2,7 +2,7 @@ // // Dynamic logic (README build order §7): CreateOpportunity/UpdateOpportunity // declare requiredWhen(primaryContact, engaged(expectedCloseValue)) — the -// shipped And/Or/Not-capable rule combinator vocabulary (morph#78), used +// shipped And/Or/Not-capable rule combinator vocabulary, used // here as a plain requiredWhen (no compound and/or/not needed for this // particular rule, since the fields available don't support the // stage-comparison this rung originally considered — see diff --git a/examples/crm/tests/test_offline_sync.cpp b/examples/crm/tests/test_offline_sync.cpp index ef3add80f..c3c8194a5 100644 --- a/examples/crm/tests/test_offline_sync.cpp +++ b/examples/crm/tests/test_offline_sync.cpp @@ -16,7 +16,7 @@ // ── GCC 16's -Warray-bounds false positive inside libstdc++'s shared_ptr ──── // -// morph#725, and the same block as `test_pipeline.cpp`'s -- which carries the +// The same block as `test_pipeline.cpp`'s -- which carries the // full measurement, the sizes, and the reason this wraps only the `#include` // block. Short form: at -O2/-O3, GCC 16 speculatively devirtualizes // `ModelFactory::create()`'s `IModelHolder::attachActionLog` @@ -95,7 +95,7 @@ struct Deal { /// Calls `onBackendChanged()` directly, from a thread with a session /// installed — the same documented divergence from the real /// `switchBackend`-posted path that `test_offline_capture.cpp`'s own -/// `reconnect()` doc comment names (morph#201): what this exercises is the +/// `reconnect()` doc comment names: what this exercises is the /// *classification* logic, the same code the supported path runs. /// @param queue The queue to drain. /// @param principal The reconnecting operator. diff --git a/examples/crm/tests/test_pipeline.cpp b/examples/crm/tests/test_pipeline.cpp index bf1871c75..98f6ecea0 100644 --- a/examples/crm/tests/test_pipeline.cpp +++ b/examples/crm/tests/test_pipeline.cpp @@ -7,7 +7,7 @@ // ── GCC 16's -Warray-bounds false positive inside libstdc++'s shared_ptr ──── // -// morph#725. On `g++ (GCC) 16.2.1` at -O2/-O3 with -Werror, this translation +// On `g++ (GCC) 16.2.1` at -O2/-O3 with -Werror, this translation // unit and `test_offline_sync.cpp` are the only two in the tree that fail to // compile, on a diagnostic raised entirely inside libstdc++: // diff --git a/examples/crm/tests/test_quote_model.cpp b/examples/crm/tests/test_quote_model.cpp index 51aa2a01e..15f6ae82a 100644 --- a/examples/crm/tests/test_quote_model.cpp +++ b/examples/crm/tests/test_quote_model.cpp @@ -325,7 +325,7 @@ TEST_CASE("QuoteModel journals its edits against the attached identity", "[crm][ } } -// ── GetQuote: registered on the wire, driven by nothing (morph#412) ────── +// ── GetQuote: registered on the wire, driven by nothing ────────────────── // // `BRIDGE_REGISTER_ACTION(crm::QuoteModel, crm::GetQuote, "GetQuote", ...)` // puts this action on the rung's wire surface, and the per-miss audit of diff --git a/examples/crm/tests/test_saved_view.cpp b/examples/crm/tests/test_saved_view.cpp index df819d30d..e5e6aab30 100644 --- a/examples/crm/tests/test_saved_view.cpp +++ b/examples/crm/tests/test_saved_view.cpp @@ -235,7 +235,7 @@ TEST_CASE("DeleteSavedView naming a nonexistent view is NotFound", "[crm][saved_ } // The sibling of `AccountModel`/`LeadModel`/`OpportunityModel`/`QuoteModel`'s -// own "journals its edits against the attached identity" case (morph#412). +// own "journals its edits against the attached identity" case. // `SavedViewModel::attachActionLog` was called by no test at all, so nothing // executed proved this model records what it writes. // From 9667bb49987e6512c7c5814cd19f55585d92972f Mon Sep 17 00:00:00 2001 From: Yaraslau Tamashevich Date: Thu, 24 Sep 2026 03:16:13 +0200 Subject: [PATCH 07/10] comments(examples/lims): state the constraint, drop the citation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The rung with the longest README in the tree. Its "Findings raised by this rung" section was ten tracker links; each entry keeps its whole body -- the `ModelKey`/`WrappedModelKey` rule-of-three argument, the `x-versionDecimalPlaces` two-keys-for-one-concept reasoning, the semicolon in a TEST_CASE name that made `ctest -L ladder-lims` report 85 against the binary's 87 -- and loses the link and the issue number. §3 decision 7's retag-vs-round argument and §7's "a lab reading replayed with no identified author is disqualifying" both stay in full: they are the reason this rung is stricter than the framework, not a record of a request. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW --- examples/lims/README.md | 128 ++++++++---------- .../lims/gui_lib/lims_qml_conversions.hpp | 4 +- examples/lims/gui_lib/result_presenter.hpp | 13 +- .../lims/include/lims/core/self_journal.hpp | 2 +- examples/lims/include/lims/db/lims_entity.hpp | 3 +- .../lims/include/lims/dto/analysis_dto.hpp | 11 +- .../lims/include/lims/dto/offline_dto.hpp | 2 +- examples/lims/include/lims/dto/result_dto.hpp | 4 +- .../lims/include/lims/models/sample_model.hpp | 6 +- .../include/lims/offline/field_outbox.hpp | 2 +- examples/lims/src/db/schema.cpp | 2 +- .../src/models/analysis_catalog_model.cpp | 2 +- examples/lims/src/models/sample_model.cpp | 4 +- examples/lims/tests/test_backend_matrix.cpp | 5 +- examples/lims/tests/test_lims_qml_surface.cpp | 2 +- examples/lims/tests/test_offline_capture.cpp | 5 +- examples/lims/tests/test_result_entry.cpp | 8 +- examples/lims/tests/test_sample_lifecycle.cpp | 4 +- .../lims/tests/test_schema_versioning.cpp | 3 +- .../lims/tests/test_verification_audit.cpp | 2 +- 20 files changed, 100 insertions(+), 112 deletions(-) diff --git a/examples/lims/README.md b/examples/lims/README.md index 43a581939..99174bd4c 100644 --- a/examples/lims/README.md +++ b/examples/lims/README.md @@ -91,8 +91,7 @@ Build order: exist and are enforced client- and server-side. **Correction (rung 6, verified against the shipped headers):** round 5's "there are no `and`/`or`/`not` combinators" is **out of date** — `andOf`/`orOf`/`notOf` - landed in commit 332f82c ("forms+qml: and/or/not rule conditions", #78), - are documented in `docs/spec/forms/forms.md` ("Compound conditions"), and + exist, are documented in `docs/spec/forms/forms.md` ("Compound conditions"), and are usable both nested in a `when` clause and directly as a top-level rule. What remains true: the vocabulary is still closed (no application lambdas), a hidden field's draft value still travels (decide @@ -145,8 +144,8 @@ transitions; journal as regulatory audit. too — `replay()`'s gate is fingerprint equality, not compatibility — and is admitted by a pass-through migration. The *wire* half (an old `MORPH_CLIENT_ONLY` client against a new server) stays open: nothing - mechanically enforces the action-evolution policy on that path until - issue #207's per-action `hello` fingerprint exists to assert on. + mechanically enforces the action-evolution policy on that path: there is no + per-action `hello` fingerprint to assert on. - **Self-conflict in the offline chain**: one field client editing the same sample twice offline — the second queued update's base version must reference the first *queued* update, not the server state, or replay @@ -155,9 +154,9 @@ transitions; journal as regulatory audit. redesign it** (round-5 correction): conversion carries the dp tag through unchanged, the renderer always submits in the canonical unit at the schema's `x-decimalPlaces`, and alternative-unit display rounds half-up. - What to test instead: (a) **retag-vs-round** — resolved upstream in - issue #159, which made `reconcileDeclaredPrecision` re-round rather than - retag; this rung keeps its own stricter rejection because the governing + What to test instead: (a) **retag-vs-round** — settled upstream: + `reconcileDeclaredPrecision` re-rounds rather than + retags; this rung keeps its own stricter rejection because the governing precision is the analysis version's runtime value, not the compile-time declared one (review D1, §7 below); (b) `x-unitAlternatives` lists **direct relation edges only**, so InvenTree-style "enter in any @@ -274,10 +273,9 @@ fabricated claim rather than a parse failure. ### 7. Over-precise readings are rejected, not retagged (§3, review D1) -Upstream issue #159 asked whether `x-decimalPlaces` "enforcement" should -round the value, be redocumented as advisory, or reject an over-precise -submission. The framework took the first option: `reconcileDeclaredPrecision` -now re-rounds on its wire dispatch paths, so storage and display agree there. +`x-decimalPlaces` "enforcement" could round the value, be advisory, or reject +an over-precise submission. The framework rounds: `reconcileDeclaredPrecision` +re-rounds on its wire dispatch paths, so storage and display agree there. A reading finer than the method supports is a claim about the instrument, so rounding it would record a measurement the analyst never made, and storing it @@ -286,8 +284,8 @@ Either way storage and display disagree, which is disqualifying in a LIMS, so this rung rejects the payload. The precision it rejects against is the **analysis version's**, not the -compiled `Quantity`'s. That is now expressed as a -`morph::forms::InstanceConstraints` (upstream issue #164) rather than a +compiled `Quantity`'s. That is expressed as a +`morph::forms::InstanceConstraints` rather than a hand-written check: the same declaration that decorated the served form's `x-decimalPlaces` reports `precisionExceeded` here, so the number the operator's renderer honoured and the number the server enforces are one @@ -324,14 +322,14 @@ row rather than from the compiled type. Revising an analysis leaves the old version's served form byte-identical, which is the ODK property the README asks for. -**Corrected by upstream issue #164.** This rung originally served the -per-version precision as a *second* key, `x-versionDecimalPlaces`, beside the -framework's `x-decimalPlaces`, because overwriting the framework's key would -have advertised a promise no code kept. Two keys for one concept, with no way -for a renderer to know which to believe, was worse than either alone. The -framework now has a seam for it: one declaration both decorates the schema and -checks the submitted reading, so the advertised number and the enforced one -cannot drift apart. `x-versionDecimalPlaces` is gone. +**Not a second key.** Serving the per-version precision as +`x-versionDecimalPlaces` beside the framework's `x-decimalPlaces` -- the shape +this needs without a framework seam, since overwriting the framework's key would +advertise a promise no code kept -- is two keys for one concept, with no way for +a renderer to know which to believe, and worse than either alone. The framework's +seam is what avoids it: one declaration both decorates the schema and checks the +submitted reading, so the advertised number and the enforced one cannot drift +apart. The form's *shape* is still compiled and therefore identical for every version: the `required` array and the `x-rules` list come from @@ -360,16 +358,17 @@ own strand, where `session::current()` is null, so every queued item is refused for want of a principal. A lab reading replayed with no identified author is what this README calls disqualifying, so failing closed is right — but it does mean the framework's own replay seam cannot carry an -authenticated replay. That is morph#201, and it was found only by -driving replay through `switchBackend` instead of calling the hook directly; -the §7 suite's own helper calls it from a thread that has a session -installed, which the framework never does. +authenticated replay. `docs/spec/offline/offline.md`'s "Conflict resolution on +replay" says so. It is only visible when replay is driven through +`switchBackend` rather than by calling the hook directly: the §7 suite's own +helper calls it from a thread that has a session installed, which the framework +never does. The write half could not use a framework seam either: there is no enqueue-on-failure hook, and the machine that must make the decision (a disconnected field client) has no model on it at all. -`include/lims/offline/field_outbox.hpp` is the app-layer answer, and -morph#197 is the finding — now dispositioned: `IMPLEMENTATION.md` rule 1 +`include/lims/offline/field_outbox.hpp` is the app-layer answer, and it is +app-layer by design rather than by omission: `IMPLEMENTATION.md` rule 1 carries a named carve-out for this seam, and `FieldOutbox` is the shape it points at (`docs/spec/offline/offline.md`, "Disposition: app-layer by design"). @@ -415,8 +414,8 @@ start flagging the client's own later updates as stale. This is where `docs/spec/offline/offline.md` puts the enforcement ("the queue … never interprets, requires, or enforces uniqueness on it — enforcement is the replay consumer's job"), and it is also the only way to be correct on all -three shipped queues, which disagree about whether they dedup at enqueue time -(morph#175). +three shipped queues, which disagree about whether they dedup at enqueue +time. The operation key is a minted random 128-bit id, per the spec's own recommendation. A counter was tried first and was wrong: `FieldOutbox` holds @@ -477,8 +476,8 @@ so nobody discovers the omission. action id and a result that does not parse. It cannot catch a payload that parses into something *else* — a renamed field decodes to a default, silently, so a trail reconstructed across a rename is confidently wrong rather than -visibly incomplete. That was morph#174's diagnosis, and the framework has -since answered it — `journal::replay()` now stamps every entry with a +visibly incomplete. The framework's answer to that is +`journal::replay()`, which stamps every entry with a payload-shape fingerprint and refuses to replay across a mismatch instead of degrading to defaults (`docs/spec/journal/journal.md`). This rung's own reconstruction above does not go through `journal::replay()` — it decodes @@ -525,7 +524,7 @@ claim the rule holds, but still submits, because a client that blocked on an unknown rule could not talk to a newer server at all. That evaluator is the third implementation of one closed vocabulary the -framework owns, which is morph#176. +framework owns; the shared corpus is what keeps the three honest. ### 19. `WorksheetModel` is not built, and the model list is corrected (§ model list) @@ -661,16 +660,17 @@ that handler is attached when it returns. `RegisterClient`'s reply is decoded (the same glaze reflection the wire used) to emit `clientRegistered`, since the form path yields raw JSON rather than a typed result — `clientRegistered` is what sets the `clientId` property -`SampleView.qml`'s "Latest client id" label binds (morph#309). With both -effects on the `submitIfValid` path itself, the typed `registerClient`/ -`registerSample` invokables that morph#287 had exempted as "the only working -path" became genuinely redundant and are gone — the surface audit in -`test_lims_qml_surface.cpp` carries no exemption for either any more. +`SampleView.qml`'s "Latest client id" label binds. With both +effects on the `submitIfValid` path itself, typed `registerClient`/ +`registerSample` invokables would be genuinely redundant — which is why they do +not exist, and why the surface audit in `test_lims_qml_surface.cpp` carries no +exemption for either. -### One dispatch path per action (morph#287, morph#309) +### One dispatch path per action -Both bridges used to publish a typed invokable *and* a schema-driven form for -the same action: `rejectSample`/`returnForRework` beside +Neither bridge publishes a typed invokable *and* a schema-driven form for the +same action. The pairs that would otherwise exist are +`rejectSample`/`returnForRework` beside `submitIfValid("RejectSample")` and its sibling, and `captureReading`/ `captureQualifier`/`resolveConflict` beside `submitIfValid("CaptureConcentration")` and `submitIfValid("ResolveConflict")`. @@ -734,21 +734,19 @@ files no uploaded report contained. Fixed here. ## Findings raised by this rung -- **[morph#163](https://github.com/LASTRADA-Software/morph/issues/163) - — `ModelKey` rejects strong id types. Closed upstream.** `BRIDGE_MODEL_KEY` - routed the key through `keyToString`, whose concept admitted only +- **`ModelKey` rejected strong id types. Closed upstream.** `BRIDGE_MODEL_KEY` + routes the key through `keyToString`, whose concept admitted only `std::integral` or `std::string`, while `IMPLEMENTATION.md` rule 3 mandates a strong id struct for entity identity. ledger and kanban carried the identical hand-written workaround; lims was the third, which is the - rule-of-three trigger. `ModelKey` now admits a strong id wrapping a raw key - (`WrappedModelKey`), and morph#183 deleted all three rungs' hand-written - blocks — `SampleModel` keys on `SampleId` itself, and an empty id is refused + rule-of-three trigger. `ModelKey` admits a strong id wrapping a raw key + (`WrappedModelKey`) now, so no rung carries a hand-written block — + `SampleModel` keys on `SampleId` itself, and an empty id is refused by `keyToString` instead of dereferenced. - **The round-5 "no `and`/`or`/`not` combinators" claim is stale** (build - order §5 above, corrected in place). They landed in commit 332f82c (#78) - and are specified in `docs/spec/forms/forms.md`. -- **[morph#164](https://github.com/LASTRADA-Software/morph/issues/164) - — a forms schema is a pure function of the compiled action type.** + order §5 above, corrected in place). They exist and are specified in + `docs/spec/forms/forms.md`. +- **a forms schema is a pure function of the compiled action type.** **Partly closed upstream.** Per-instance *values* now reach the framework's own keys through `morph::forms::InstanceConstraints`: the two analysis versions declaring 3 and 1 decimal places serve `"x-decimalPlaces":3` and @@ -758,8 +756,7 @@ files no uploaded report contained. Fixed here. still a function of the compiled type, so a rung whose definitions are data needs one compiled action per unit family rather than per analysis. That is what rung 7's runtime custom fields run into head-on. -- **[morph#174](https://github.com/LASTRADA-Software/morph/issues/174) - — a journal entry from an older build decodes leniently to defaults, with no +- **a journal entry from an older build decodes leniently to defaults, with no signal.** **Closed at the framework level; this rung's own audit reconstruction has not adopted it.** `journal::replay()` now stamps every entry with `morph::model::payloadFingerprint()` and throws @@ -772,9 +769,9 @@ files no uploaded report contained. Fixed here. field still decodes to its default with no signal — the finding entry below, §17 in "What that does not cover, stated plainly," is accordingly still live for this rung specifically, even though the framework question it - raised is answered. + raised is answered. (Filed; closed upstream.) - **A schema's `required` array can silently contradict its own `x-rules`.** - **Closed (morph#165).** `schemaJson`'s required-by-default rule put both + **Closed.** `schemaJson`'s required-by-default rule put both `value` and `qualifier` in `required` while the `exactlyOneOf` entry beside them said at most one may be engaged — an unsatisfiable form. `schemaJson()` now rejects exactly this at generation, throwing @@ -788,16 +785,14 @@ files no uploaded report contained. Fixed here. (`include/lims/dto/result_dto.hpp`) explains why it is still needed even though omitting it is now a loud error rather than a silent one. Guarded here by a test asserting `required` is exactly `["analysisVersionId"]`. -- **[morph#175](https://github.com/LASTRADA-Software/morph/issues/175) - — the three shipped `IOfflineQueue`s disagree about repeated idempotency +- **the three shipped `IOfflineQueue`s disagree about repeated idempotency keys.** **Closed.** `InMemoryOfflineQueue` admits the duplicate; `FileOfflineQueue` and `SqliteOfflineQueue` dedup — a deliberate strengthening the base contract permits but did not document. The interface header (`include/morph/offline/offline_queue.hpp`) now states the hit semantics explicitly: a dedup hit is first-write-wins with silent payload loss, matching what `docs/spec/offline/offline.md` already said. -- **[morph#197](https://github.com/LASTRADA-Software/morph/issues/197) - — the offline write path has no model-side seam.** Rule 1 says all domain +- **the offline write path has no model-side seam.** Rule 1 says all domain logic lives in models; the offline spec says enqueue-on-failure is the application's job at the dispatch site; and a disconnected field client has no model to put it in anyway. `FieldOutbox` is this rung's app-layer answer, @@ -806,14 +801,12 @@ files no uploaded report contained. Fixed here. carve-out and `docs/spec/offline/offline.md` records the reasoning and the boundary; a framework primitive is reconsidered when a third rung grows its own enqueue path. -- **[morph#172](https://github.com/LASTRADA-Software/morph/issues/172) - — `MORPH_BUILD_OFFLINE_SQLITE=ON` breaks the build on macOS with a non-Apple +- **`MORPH_BUILD_OFFLINE_SQLITE=ON` breaks the build on macOS with a non-Apple clang.** `FindSQLite3` resolves the SDK's whole `/usr/include`, which is then injected as `-isystem` ahead of libc++'s own headers. The repo's own `morph_offline_sqlite_tests` target fails identically, which is why the durable queue had never been built here before. -- **[morph#176](https://github.com/LASTRADA-Software/morph/issues/176) - — `x-rules` has one client-side evaluator and no shared corpus.** **Closed.** +- **`x-rules` has one client-side evaluator and no shared corpus.** **Closed.** A shared corpus now pins the compiled evaluator (`morph::forms::allRulesSatisfied`) to the QML one: one checked-in file, `src/qt/forms/tests/data/rule_corpus.json`, is driven through both — @@ -824,23 +817,20 @@ files no uploaded report contained. Fixed here. `tests/test_forms_rule_agreement.cpp`). A non-QML client still has to reimplement the vocabulary; the corpus is what keeps that reimplementation honest against the compiled evaluator's behavior. -- **[morph#173](https://github.com/LASTRADA-Software/morph/issues/173) - — a ladder test whose name contains a semicolon never gets its +- **a ladder test whose name contains a semicolon never gets its `ladder-` label.** `morph_add_rung`'s re-labelling step iterates `IN LISTS`, which splits the name, so `set_tests_properties` applies to nothing and nothing warns. Found here the hard way: `ctest -L ladder-lims` reported 85 cases while the binary reported 87. Two lims cases were renamed; - 12 pre-existing ones repo-wide are still affected. -- **[morph#199](https://github.com/LASTRADA-Software/morph/issues/199) - — a `Quantity`'s exact decimal can only be rendered with its unit appended.** + 12 pre-existing ones repo-wide are still affected. (Filed.) +- **a `Quantity`'s exact decimal can only be rendered with its unit appended.** **Closed.** `toDecimalString(quantity)` (`include/morph/util/quantity.hpp`) is now the public decimal-only renderer — the exact numeric half of `toString`, no unit suffix, `"N/A"` for an empty quantity — so a view that places the number and the unit separately (a table with the unit in its column header, a right-aligned suffix) asks for the two halves independently instead of concatenating them and chopping the suffix back off. -- **[morph#201](https://github.com/LASTRADA-Software/morph/issues/201) - — `Model::onBackendChanged()` runs with no session.** **Closed as a +- **`Model::onBackendChanged()` runs with no session.** **Closed as a documentation correction, not a code gap.** `LocalBackend` never consults an authorizer at all, so no version of this hook could ever carry a *verified* principal — a session-plumbing fix was the wrong ask. `switchBackend` still diff --git a/examples/lims/gui_lib/lims_qml_conversions.hpp b/examples/lims/gui_lib/lims_qml_conversions.hpp index c0e8e61a0..4169a9ed9 100644 --- a/examples/lims/gui_lib/lims_qml_conversions.hpp +++ b/examples/lims/gui_lib/lims_qml_conversions.hpp @@ -42,7 +42,7 @@ namespace lims::gui { /// An id as the plain number QML rows and invokables carry, `-1` when -/// unengaged. Re-exported rather than redefined (morph#169) so this rung's +/// unengaged. Re-exported rather than redefined so this rung's /// `-1` is the same named `kNoId` the other four rungs publish, and so the /// name stays `lims::gui::idNumber` for the callers and tests that use it. using ::morph::ladder::gui::idNumber; @@ -82,7 +82,7 @@ template /// type's: `toDecimalString` reports an absent value as `"N/A"` — so that /// `toString == toDecimalString + display` holds for every value — and a QML /// view showing an unfilled measurement wants an empty cell, not the letters -/// `N/A` baked into its text. See morph#199. +/// `N/A` baked into its text. /// @tparam Q The `Quantity` specialisation. /// @param quantity The value to render. /// @return Its shortest exact decimal at its own precision, or `""`. diff --git a/examples/lims/gui_lib/result_presenter.hpp b/examples/lims/gui_lib/result_presenter.hpp index 601c787d0..bc7177f83 100644 --- a/examples/lims/gui_lib/result_presenter.hpp +++ b/examples/lims/gui_lib/result_presenter.hpp @@ -41,14 +41,13 @@ namespace lims::gui { /// a schema-driven form body -- `submitIfValid("CaptureConcentration", ...)` /// -- so the number travels as the exact rational the shipped renderer built /// from the served `x-decimalPlaces`, and the model checks that precision -/// against the analysis version's own declaration. An earlier -/// `captureReading(versionId, double, ...)` entry point converted the value -/// with `Concentration::fromDouble` at the field's declared precision, which -/// was exact but *rounding*: it made the model's over-precision refusal (the -/// rung README's §3 decision 7) unreachable from the GUI, and it put the only +/// against the analysis version's own declaration. There is deliberately no +/// `captureReading(versionId, double, ...)` overload: converting the value with +/// `Concentration::fromDouble` at the field's declared precision is exact but +/// *rounding*, which makes the model's over-precision refusal (the rung +/// README's §3 decision 7) unreachable from the GUI, and it would put the only /// `double` on this rung's QML surface, against the convention -/// `lims_qml_conversions.hpp` states. It was deleted rather than documented -/// (morph#287). +/// `lims_qml_conversions.hpp` states. class ResultPresenter : public ::morph::ladder::gui::Presenter { Q_OBJECT public: diff --git a/examples/lims/include/lims/core/self_journal.hpp b/examples/lims/include/lims/core/self_journal.hpp index 23f878d2f..17b0e28a7 100644 --- a/examples/lims/include/lims/core/self_journal.hpp +++ b/examples/lims/include/lims/core/self_journal.hpp @@ -170,7 +170,7 @@ class SelfJournal { // this rung records is *unstamped*, and a later replay against a // renamed field would decode it to a default and report the wrong // state confidently -- the failure this rung's README names as the - // ladder's open question (issue #174). Empty for an action whose + // ladder's open question. Empty for an action whose // ActionTraits is hand-written; see docs/spec/journal/journal.md, // "Payload schema fingerprint". entry.schema = ::morph::model::detail::actionPayloadSchema(); diff --git a/examples/lims/include/lims/db/lims_entity.hpp b/examples/lims/include/lims/db/lims_entity.hpp index 594ce780d..2f03570e6 100644 --- a/examples/lims/include/lims/db/lims_entity.hpp +++ b/examples/lims/include/lims/db/lims_entity.hpp @@ -172,8 +172,7 @@ struct OperatorRecord { /// and never interprets, requires, or enforces uniqueness on it"). This table /// is that enforcement, made durable: a second delivery of the same logical /// field update is skipped rather than acted on again — which matters because -/// the shipped queues disagree about whether they dedup at enqueue time (see -/// morph#175). +/// the shipped queues disagree about whether they dedup at enqueue time. /// /// A row is written once the operation reaches a *terminal* decision, which is /// applied **or** flagged as a conflict — not only applied. A flagged item is diff --git a/examples/lims/include/lims/dto/analysis_dto.hpp b/examples/lims/include/lims/dto/analysis_dto.hpp index 14d0da650..dd77d7102 100644 --- a/examples/lims/include/lims/dto/analysis_dto.hpp +++ b/examples/lims/include/lims/dto/analysis_dto.hpp @@ -46,11 +46,12 @@ struct AnalysisBound { /// analysis version — the single declaration that both decorates the /// served schema and checks the submitted reading. /// -/// This is the rung's use of `morph::forms::InstanceConstraints` (upstream -/// issue #164). Before it existed the version's precision had to be served as -/// a second, app-private key beside the framework's `x-decimalPlaces` and the -/// check re-implemented by hand in `SampleModel`, and the specification range -/// could only be served as a key no code anywhere read. +/// This is the rung's use of `morph::forms::InstanceConstraints`, the +/// framework's own instance-constraint seam. Without it the version's precision +/// would have to be served as a second, app-private key beside the framework's +/// `x-decimalPlaces` with the check re-implemented by hand in `SampleModel`, +/// and the specification range could only be served as a key no code anywhere +/// read. /// @param field The wire (JSON) field name the reading is submitted under. /// @param decimalPlaces The version's declared decimal places. /// @param specLow The version's inclusive lower specification bound, if any. diff --git a/examples/lims/include/lims/dto/offline_dto.hpp b/examples/lims/include/lims/dto/offline_dto.hpp index 4f7d61cf0..8a98ac535 100644 --- a/examples/lims/include/lims/dto/offline_dto.hpp +++ b/examples/lims/include/lims/dto/offline_dto.hpp @@ -138,7 +138,7 @@ struct QueuedCapture { /// at-most-once *whatever* delivered it — the queue, a re-dispatch, or a /// journal replay. The queue itself is documented not to enforce /// uniqueness, and the shipped implementations disagree about whether they - /// do anyway (morph#175), so the enforcement has to live where + /// do anyway, so the enforcement has to live where /// it is asked for: in the consumer. OperationKey operationKey; diff --git a/examples/lims/include/lims/dto/result_dto.hpp b/examples/lims/include/lims/dto/result_dto.hpp index 276633eaf..aeca01480 100644 --- a/examples/lims/include/lims/dto/result_dto.hpp +++ b/examples/lims/include/lims/dto/result_dto.hpp @@ -221,7 +221,7 @@ struct CaptureConcentration { /// contradict the `x-rules` entry beside them: a renderer honouring /// `required` would demand both fields, and a payload satisfying it would /// then fail `exactlyOneOf` on the server. The opt-out below is what makes - /// the rule the only gate on the pair; since morph#165 `schemaJson` also + /// the rule the only gate on the pair; `schemaJson` also /// *rejects* the contradiction (`UnsatisfiableFormError`) instead of /// serving a form nobody can submit, so omitting it is now a loud error /// rather than a silent one. @@ -284,7 +284,7 @@ struct ResultView { /// capture would destroy the observation. What it must never be is /// *silent* — before the framework could name a bound living in a version /// row, the range round-tripped to the client and back and was acted on - /// nowhere (upstream issue #164). + /// nowhere. /// /// Always `false` for a non-reading: a qualifier makes no numeric claim to /// be in or out of specification. diff --git a/examples/lims/include/lims/models/sample_model.hpp b/examples/lims/include/lims/models/sample_model.hpp index 9d81277e5..14d99fef8 100644 --- a/examples/lims/include/lims/models/sample_model.hpp +++ b/examples/lims/include/lims/models/sample_model.hpp @@ -228,7 +228,7 @@ class SampleModel { /// is skipped. That enforcement belongs here, not in the queue /// (`docs/spec/offline/offline.md`), and it is what makes replay correct /// against every shipped `IOfflineQueue` regardless of whether that - /// implementation happens to dedup at enqueue time (morph#175). + /// implementation happens to dedup at enqueue time. /// @param action The queued update, with the base version it assumed. /// @return What replay did, and the conflict id if it flagged one. /// @throws ValidationError if the envelope is not well-formed. @@ -396,8 +396,8 @@ BRIDGE_REGISTER_ACTION(lims::SampleModel, lims::GetAuditTrail, "GetAuditTrail", // `std::int64_t` this rung declared by hand while `morph::model::ModelKey` // still admitted only `std::integral`/`std::string`. That restriction is what // forced the hand-written specialisations here, in `ledger::LedgerModel` and -// in `kanban::BoardModel`; morph#163 widened the concept to admit a strong id -// wrapping a raw key, and morph#183 deleted all three rungs' blocks. +// in `kanban::BoardModel`. The concept admits a strong id wrapping a raw key, +// so no rung carries such a block any more. // // Their bodies did `keyToString(*action.sampleId)` -- `operator*` on a // possibly-disengaged `std::optional`, undefined behaviour for an action diff --git a/examples/lims/include/lims/offline/field_outbox.hpp b/examples/lims/include/lims/offline/field_outbox.hpp index 53e10ec94..67d163c58 100644 --- a/examples/lims/include/lims/offline/field_outbox.hpp +++ b/examples/lims/include/lims/offline/field_outbox.hpp @@ -25,7 +25,7 @@ /// rung's field client is deployed remotely: its models live server-side /// behind Lightweight/ODBC and are reached over a `Bridge`, so the one machine /// that must decide "queue this instead of sending it" is the one machine with -/// no model on it. Filed as morph#197 and dispositioned there: rule 1 now +/// no model on it. App-layer by design rather than by omission: rule 1 /// carries a named carve-out for the offline write-path enqueue seam, and this /// class is the carve-out's reference shape for the domain-shaped half (see /// `docs/spec/offline/offline.md`, "Disposition: app-layer by design"). The diff --git a/examples/lims/src/db/schema.cpp b/examples/lims/src/db/schema.cpp index 90546ee36..7ff71cfd5 100644 --- a/examples/lims/src/db/schema.cpp +++ b/examples/lims/src/db/schema.cpp @@ -8,7 +8,7 @@ namespace lims::db { void configure(const std::string& connectionString) { - // morph#740: nothing in Lightweight stops a pooled DataMapper from being + // Nothing in Lightweight stops a pooled DataMapper from being // returned with a transaction still open on it -- `DataMapperPool::Return` // does no transaction cleanup, and the cost lands on the next, unrelated // borrower as a 60s stall and a `database is locked` it did not cause. diff --git a/examples/lims/src/models/analysis_catalog_model.cpp b/examples/lims/src/models/analysis_catalog_model.cpp index 035cdf92f..3b8ff9890 100644 --- a/examples/lims/src/models/analysis_catalog_model.cpp +++ b/examples/lims/src/models/analysis_catalog_model.cpp @@ -75,7 +75,7 @@ void applyBounds(db::AnalysisVersionRecord& row, const Action& action) { /// data" boundary the README predicts. /// /// What is no longer app-private is the part the framework has a vocabulary -/// for. `morph::forms::InstanceConstraints` (upstream issue #164) carries the +/// for. `morph::forms::InstanceConstraints` carries the /// version's precision and specification range into `x-decimalPlaces`, /// `x-minimum` and `x-maximum` — the framework's own keys — and the same /// declaration is what `SampleModel` checks a submitted reading against, so diff --git a/examples/lims/src/models/sample_model.cpp b/examples/lims/src/models/sample_model.cpp index 3e025f642..4d5de0d18 100644 --- a/examples/lims/src/models/sample_model.cpp +++ b/examples/lims/src/models/sample_model.cpp @@ -425,7 +425,7 @@ ResultView SampleModel::applyCapture(SampleId sampleId, const CaptureConcentrati // does so through `Rational`, never a `double`. const auto reading = *capture.value * dilutionMultiplier; - // One declaration, both jobs (upstream issue #164). This is the same + // One declaration, both jobs. This is the same // `versionConstraints(...)` the catalogue built to decorate the served // form's `x-decimalPlaces` / `x-minimum` / `x-maximum`, so the numbers // the operator's renderer honoured are literally the numbers checked @@ -594,7 +594,7 @@ ReplayCaptureResult SampleModel::execute(const QueuedCapture& action) { // At-most-once, enforced here because the queue is documented not to // enforce it -- and because the shipped queues disagree about whether - // they do anyway (morph#175). + // they do anyway. if (alreadyDecided(*action.operationKey)) { return ReplayCaptureResult{.outcome = ReplayOutcome::Skipped, .sampleId = action.sampleId}; } diff --git a/examples/lims/tests/test_backend_matrix.cpp b/examples/lims/tests/test_backend_matrix.cpp index 889281ce1..0457c5d71 100644 --- a/examples/lims/tests/test_backend_matrix.cpp +++ b/examples/lims/tests/test_backend_matrix.cpp @@ -200,7 +200,7 @@ TEST_CASE("An offline capture replays through the bridge under its operator's ow // actually drives it: the client drains its own queue and re-dispatches // each item as an ordinary action through its authenticated `Bridge`. // That is what makes `QueuedCapture`'s `capturedBy` check mean anything — - // see this rung's README §7 decision and morph#201 for why the + // see this rung's README §7 decision for why the // framework's own `onBackendChanged()` drain cannot carry a session. const auto mode = GENERATE(Mode::Local, Mode::LocalSingleThread, Mode::Socket); CAPTURE(mode); @@ -301,8 +301,7 @@ TEST_CASE("onBackendChanged fires on switchBackend, and fails closed with no ses // for this rung: a lab reading replayed with no identified author is // exactly what the README calls disqualifying. But it does mean the // framework's own replay seam cannot carry an authenticated replay, which - // is why §7's supported path is the re-dispatch above. See - // morph#201. + // is why §7's supported path is the re-dispatch above. DbFixture fixture; auto queue = std::make_shared(); diff --git a/examples/lims/tests/test_lims_qml_surface.cpp b/examples/lims/tests/test_lims_qml_surface.cpp index 42442dd90..b4ff56e15 100644 --- a/examples/lims/tests/test_lims_qml_surface.cpp +++ b/examples/lims/tests/test_lims_qml_surface.cpp @@ -76,7 +76,7 @@ TEST_CASE("Every lims bridge exposes exactly the surface gui/qml binds, and noth // the shared handler, so it leaves that handler attached to the new sample // the same way the typed call did (`BridgeHandler::execute`'s `ResultKeyed` // branch). Neither typed invokable added anything beyond that any more, so - // both are gone (morph#309, closing morph#287's last two exemptions). + // both are gone, so neither needs an exemption here. // `submitIfValid` on both bridges is called from the shipped `MorphForms` // renderer's QML (`src/qt/forms/qml/DynamicForm.qml`'s `submit()` and its diff --git a/examples/lims/tests/test_offline_capture.cpp b/examples/lims/tests/test_offline_capture.cpp index 855c87b87..8d14f48f8 100644 --- a/examples/lims/tests/test_offline_capture.cpp +++ b/examples/lims/tests/test_offline_capture.cpp @@ -93,7 +93,7 @@ struct Lab { /// the model's own strand, where `session::current()` is null, and every item /// is then refused for want of a principal. That is pinned separately, in /// `test_backend_matrix.cpp`'s "onBackendChanged fires on switchBackend, and -/// fails closed with no session", and filed as morph#201. What this +/// fails closed with no session". What this /// helper exercises is the *classification* logic — base-version comparison, /// conflict flagging, at-most-once — which is the same code the supported /// re-dispatch path runs, and which `test_backend_matrix.cpp` also drives @@ -729,7 +729,8 @@ TEST_CASE("The durable queue dedups a re-enqueued operation where the in-memory // retried a local write, say. `IOfflineQueue`'s documented contract is // that the queue "never interprets, requires, or enforces uniqueness" on // the key, but the two shipped implementations do not agree about that. - // See morph#175; this test pins the divergence so a future change + // The three shipped queues disagree about enqueue-time dedup; this test + // pins the divergence so a future change // to either one is noticed here rather than in a lab. const auto path = freshQueuePath("dedup"); morph::offline::SqliteOfflineQueue durable{path}; diff --git a/examples/lims/tests/test_result_entry.cpp b/examples/lims/tests/test_result_entry.cpp index 4356666e6..28f085166 100644 --- a/examples/lims/tests/test_result_entry.cpp +++ b/examples/lims/tests/test_result_entry.cpp @@ -153,7 +153,7 @@ TEST_CASE("The served schema carries the encoding, the precision and the entry u // both there, where they contradict the `x-rules` entry beside them: a // renderer honouring `required` would demand both fields, and a payload // satisfying that demand then fails `exactlyOneOf` on the server. The - // encoding opts out via `optionalFields`; since morph#165, + // encoding opts out via `optionalFields`; // `schemaJson()` would *throw* // `UnsatisfiableFormError` if it did not, so this assertion now pins the // shape of the opt-out rather than its existence. @@ -395,9 +395,9 @@ TEST_CASE("An over-precise reading is rejected, not silently retagged", "[lims][ const auto nitrate = defineNitrate(catalog); // 3 decimal places sampleAtWork(model); - // 1.23456 needs five decimals. Issue #159 made morph's `x-decimalPlaces` - // enforcement *round* on its wire dispatch paths rather than retag, so - // storage and display no longer disagree there. This rung is still + // 1.23456 needs five decimals. morph's own `x-decimalPlaces` enforcement + // *rounds* on its wire dispatch paths rather than retagging, so storage and + // display agree there. This rung is // stricter: the governing precision is the analysis version's runtime // decimalPlaces (schema-versioned data, not the compile-time // Quantity), and rounding a reading the method cannot support diff --git a/examples/lims/tests/test_sample_lifecycle.cpp b/examples/lims/tests/test_sample_lifecycle.cpp index dad9b4dc3..a01635774 100644 --- a/examples/lims/tests/test_sample_lifecycle.cpp +++ b/examples/lims/tests/test_sample_lifecycle.cpp @@ -342,7 +342,7 @@ TEST_CASE("Registering a sample or a client requires a principal and well-formed } // ═════════════════════════════════════════════════════════════════════════ -// SampleModel's primary key (morph#183) +// SampleModel's primary key // // No DbFixture below this line: `ActionKeyTraits::key()` is a pure // function over the action's own fields, and that is the point -- key @@ -396,7 +396,7 @@ TEST_CASE("Every keyed SampleModel action encodes exactly the key its hand-writt } TEST_CASE("An empty sampleId fails key extraction instead of routing to a garbage instance", "[lims][sample][key]") { - // The failure morph#183 calls out by name. The hand-written bodies were + // The failure a hand-written trait is exposed to: its body is // `keyToString(*action.sampleId)`, and `SampleId::operator*` is // `return *value;` on a `std::optional` (lims/core/types.hpp) -- // undefined behaviour for a disengaged id, which on a plain libc++ hands diff --git a/examples/lims/tests/test_schema_versioning.cpp b/examples/lims/tests/test_schema_versioning.cpp index a5eda456f..1bbcb278e 100644 --- a/examples/lims/tests/test_schema_versioning.cpp +++ b/examples/lims/tests/test_schema_versioning.cpp @@ -1,8 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // // Schema versioning (README build order §4, review D4 — mandatory, no socket -// needed). The claim under test is a *split* one, and upstream issue #164 -// moved where the split falls: +// needed). The claim under test is a *split* one: // // values are version-bound; structure is not. // diff --git a/examples/lims/tests/test_verification_audit.cpp b/examples/lims/tests/test_verification_audit.cpp index 084b10224..b9d8a02b3 100644 --- a/examples/lims/tests/test_verification_audit.cpp +++ b/examples/lims/tests/test_verification_audit.cpp @@ -478,7 +478,7 @@ TEST_CASE("A renamed payload field decodes to a default, silently -- the payload // The `Unreadable` machinery above cannot catch this: the payload *does* // decode, it just decodes to something else. Only a per-entry payload - // version could, and this rung does not have one -- see morph#174. + // version could, and this rung does not have one. } TEST_CASE("A verification appears in the sample's own audit trail", "[lims][audit]") { From 2f8be454e8fbf1f73837200b726fcae3f114aedb Mon Sep 17 00:00:00 2001 From: Yaraslau Tamashevich Date: Thu, 24 Sep 2026 03:24:09 +0200 Subject: [PATCH 08/10] =?UTF-8?q?comments(examples):=20state=20the=20const?= =?UTF-8?q?raint,=20drop=20the=20citation=20=E2=80=94=20polls,=20pastebin?= =?UTF-8?q?=20and=20bookmarks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The three remaining rungs. Kept: pastebin's sqliteodbc `RETURNING`/`SQLFetch` finding (the upstream Lightweight issue stays -- it is a third-party defect record, not this tracker) and the 1-in-1,640 exhaustion arithmetic behind `paste_id_source.hpp`'s seam; polls's `messagesPerSecond` correction, restated as what a silently dropped frame would cost rather than as what changed. Three user-visible strings changed, all of the same shape: the `fromRowId` refusals in `polls::OptionId`, `polls::PollEventId` and `kanban::BoardEventId` ended `... sentinel (morph#NNN)` and now end `... sentinel`. Nothing reads them -- grepped for the message text and for `unrepresentable` across the tree; the only assertions on these paths are `CHECK_THROWS_AS` on the exception type. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW --- examples/bookmarks/README.md | 2 +- .../gui_lib/bookmark_qml_bridges.hpp | 8 +-- .../include/bookmarks/core/types.hpp | 2 +- .../include/bookmarks/dto/bookmark_dto.hpp | 2 +- examples/bookmarks/src/db/schema.cpp | 12 ++--- .../bookmarks/src/models/bookmark_model.cpp | 13 +++-- examples/bookmarks/src/server/main.cpp | 6 +-- examples/bookmarks/tests/test_app.cpp | 3 +- .../bookmarks/tests/test_bookmark_model.cpp | 11 ++-- examples/pastebin/README.md | 9 ++-- .../pastebin/gui_lib/paste_qml_bridges.hpp | 2 +- .../include/pastebin/dto/paste_dto.hpp | 2 +- .../pastebin/models/paste_id_source.hpp | 6 +-- examples/pastebin/include/pastebin/units.hpp | 4 +- examples/pastebin/src/db/schema.cpp | 12 ++--- examples/pastebin/src/models/paste_model.cpp | 2 +- examples/pastebin/tests/test_paste_model.cpp | 28 +++++----- examples/polls/README.md | 51 +++++++++---------- examples/polls/gui_lib/poll_qml_bridges.cpp | 2 +- examples/polls/include/polls/core/types.hpp | 25 +++++---- examples/polls/src/db/schema.cpp | 12 ++--- examples/polls/tests/test_polls_types.cpp | 2 +- .../tests/test_shared_instance_lifecycle.cpp | 29 +++++------ 23 files changed, 116 insertions(+), 129 deletions(-) diff --git a/examples/bookmarks/README.md b/examples/bookmarks/README.md index 266c34759..46a5ab8ce 100644 --- a/examples/bookmarks/README.md +++ b/examples/bookmarks/README.md @@ -160,7 +160,7 @@ Actions, in build order: formalizes the full event-queue design) — there is no existing polling/event-sequencing precedent anywhere in the framework to reuse; this rung builds it from a `ChangesCursor` query (a millisecond timestamp paired - with a same-instant id tie-break, not a bare `Timestamp` — issue #43's fix + with a same-instant id tie-break, not a bare `Timestamp` — the fix for the boundary case a timestamp-only cursor can silently drop), deliberately minimal otherwise. The action exists and is tested; the shipped client does not dispatch it (see the client-gaps list). diff --git a/examples/bookmarks/gui_lib/bookmark_qml_bridges.hpp b/examples/bookmarks/gui_lib/bookmark_qml_bridges.hpp index 03bb3a3c0..83866e2ee 100644 --- a/examples/bookmarks/gui_lib/bookmark_qml_bridges.hpp +++ b/examples/bookmarks/gui_lib/bookmark_qml_bridges.hpp @@ -105,10 +105,10 @@ namespace bookmarks::gui { /// reaches here: `BookmarkPresenter`/`TagPresenter`/`SharedFeedPresenter` /// inherit `Presenter::track()`, which re-checks a `QPointer` before touching /// the presenter (`examples/common/gui/presenter.hpp` — added for a real -/// AddressSanitizer `stack-use-after-scope`, morph#137), and the three -/// `*Bridge` classes over them relay through Qt signal/slot connections, which -/// Qt severs when either end is destroyed. `FormsBridge` dispatches directly -/// and goes through neither, so it takes the framework's general answer: +/// AddressSanitizer `stack-use-after-scope`), and the three `*Bridge` classes +/// over them relay through Qt signal/slot connections, which Qt severs when +/// either end is destroyed. `FormsBridge` dispatches directly and goes through +/// neither, so it takes the framework's general answer: /// `morph::async::CallbackScope` (`docs/spec/core/callback_scope.md`) as a /// **last-declared member**, with every `this`-capturing callback wrapped in /// `_callbacks.guard(...)`. Members are destroyed in reverse declaration diff --git a/examples/bookmarks/include/bookmarks/core/types.hpp b/examples/bookmarks/include/bookmarks/core/types.hpp index 528f817b7..4881b199d 100644 --- a/examples/bookmarks/include/bookmarks/core/types.hpp +++ b/examples/bookmarks/include/bookmarks/core/types.hpp @@ -106,7 +106,7 @@ struct Cursor { [[nodiscard]] auto operator<=>(const Cursor&) const noexcept = default; }; -/// @brief `GetChangesSince`'s cursor (issue #43): a millisecond timestamp +/// @brief `GetChangesSince`'s cursor: a millisecond timestamp /// alone cannot be a correct "since" boundary, because a strict `>` /// comparison on `updated_at_ms` silently drops a write that lands in /// the *same millisecond* as the previous poll's cursor -- plausible diff --git a/examples/bookmarks/include/bookmarks/dto/bookmark_dto.hpp b/examples/bookmarks/include/bookmarks/dto/bookmark_dto.hpp index 9c2627e42..160f0b1f7 100644 --- a/examples/bookmarks/include/bookmarks/dto/bookmark_dto.hpp +++ b/examples/bookmarks/include/bookmarks/dto/bookmark_dto.hpp @@ -195,7 +195,7 @@ struct GetChangesSinceResult { /// @brief The boundary this poll ran to, captured *before* the query /// itself (`BookmarkModel::execute`'s own doc comment, Task 7, /// has the full argument for why) — the next poll's `since`. - /// `ChangesCursor` (issue #43), not a bare `Timestamp`: a + /// `ChangesCursor`, not a bare `Timestamp`: a /// millisecond-resolution timestamp alone cannot distinguish a /// write that lands in the exact same millisecond as this /// instant from one that happened strictly before it. diff --git a/examples/bookmarks/src/db/schema.cpp b/examples/bookmarks/src/db/schema.cpp index 15465aaf9..b4284bb22 100644 --- a/examples/bookmarks/src/db/schema.cpp +++ b/examples/bookmarks/src/db/schema.cpp @@ -9,12 +9,12 @@ namespace bookmarks::db { void setup(const std::string& connectionString) { - // morph#740: nothing in Lightweight stops a pooled DataMapper from being - // returned with a transaction still open on it -- `DataMapperPool::Return` - // does no transaction cleanup, and the cost lands on the next, unrelated - // borrower as a 60s stall and a `database is locked` it did not cause. - // Installing the audit here, at the one point every rung's process - // configures its database, turns that into an abort at the leak. See + // Nothing in Lightweight stops a pooled DataMapper from being returned with + // a transaction still open on it -- `DataMapperPool::Return` does no + // transaction cleanup, and the cost lands on the next, unrelated borrower + // as a 60s stall and a `database is locked` it did not cause. Installing + // the audit here, at the one point every rung's process configures its + // database, turns that into an abort at the leak. See // examples/common/db/pool_transaction_audit.hpp. (void)::morph::ladder::db::installPoolTransactionAudit(); diff --git a/examples/bookmarks/src/models/bookmark_model.cpp b/examples/bookmarks/src/models/bookmark_model.cpp index 776b4b8e0..7a2b5e3ca 100644 --- a/examples/bookmarks/src/models/bookmark_model.cpp +++ b/examples/bookmarks/src/models/bookmark_model.cpp @@ -61,7 +61,7 @@ namespace { } /// @brief `morph::offline::IReplayLedger` over `db::ImportedOpRecord` -/// (morph#226) -- the first rung migrated onto the promoted interface. +/// -- the first rung migrated onto the promoted interface. /// /// A file-local class rather than its own header: it has exactly one /// consumer (`ImportBookmarks::execute`, below), and every one of its @@ -544,12 +544,11 @@ GetChangesSinceResult BookmarkModel::execute(const GetChangesSince& action) { action.since.timestampMs.hasValue() ? (*action.since.timestampMs).value.time_since_epoch().count() : 0; const std::uint64_t sinceLastId = static_cast(action.since.lastId.value_or(0)); - // See ChangesCursor's doc comment (issue #43): a strict `updatedAtMs > - // sinceMs` alone drops a write landing in the exact same millisecond as - // `sinceMs`. The id tie-break recovers it without over-including: any - // row strictly after sinceMs qualifies outright; a row *at* sinceMs - // qualifies only if its id is past the last one already delivered at - // that same instant. + // See ChangesCursor's doc comment: a strict `updatedAtMs > sinceMs` alone + // drops a write landing in the exact same millisecond as `sinceMs`. The id + // tie-break recovers it without over-including: any row strictly after + // sinceMs qualifies outright; a row *at* sinceMs qualifies only if its id + // is past the last one already delivered at that same instant. auto mapper = ::Lightweight::GlobalDataMapperPool().Acquire(); auto rows = mapper->Query() diff --git a/examples/bookmarks/src/server/main.cpp b/examples/bookmarks/src/server/main.cpp index c7a948752..a40171ff7 100644 --- a/examples/bookmarks/src/server/main.cpp +++ b/examples/bookmarks/src/server/main.cpp @@ -26,9 +26,9 @@ /// public seam for this — exactly the class of reach-in /// `examples/common/testkit` migrated away from onto public seams /// (`Completion::makeSettleable()`, `BridgeHandler::whenBound()`, the -/// `QtWebSocketBackend(url, tls, cfg)` overload) once #55's public seams -/// existed; adding a new one here from an *example* would be a step -/// backward, not forward. The alternative — an internal client with a +/// `QtWebSocketBackend(url, tls, cfg)` overload) as soon as public seams +/// existed for them; adding a new reach-in here from an *example* would be a +/// step backward, not forward. The alternative — an internal client with a /// minted service token, the shape `App`'s own metadata worker uses — is /// real infrastructure that `LADDER.md` already assigns to rung 4's /// `action_driver` generators. Demo data is therefore created through the diff --git a/examples/bookmarks/tests/test_app.cpp b/examples/bookmarks/tests/test_app.cpp index b63536770..683ce8e3d 100644 --- a/examples/bookmarks/tests/test_app.cpp +++ b/examples/bookmarks/tests/test_app.cpp @@ -454,8 +454,7 @@ TEST_CASE("App::stopBackgroundJobs is idempotent, and ~App still stops the timer TEST_CASE("App teardown survives fetch dispatches still outstanding (the member-order guard)", "[bookmarks][app]") { // The guard for `App`'s member declaration order, which is the entire // protection against a `post()` on a freed `QtExecutor` - // (`bookmarks/app/app.hpp`'s comment above `_fetchExecutor`, and morph#127, - // which was a shipped bug of exactly that class). `_fetchExecutor` is + // (`bookmarks/app/app.hpp`'s comment above `_fetchExecutor`). `_fetchExecutor` is // declared *first*, so it is destroyed *last* — after `~_pool` has joined // its worker threads. Move it to its natural reading position at the end of // the member list and this case segfaults deterministically: diff --git a/examples/bookmarks/tests/test_bookmark_model.cpp b/examples/bookmarks/tests/test_bookmark_model.cpp index 4531100c8..7728bac02 100644 --- a/examples/bookmarks/tests/test_bookmark_model.cpp +++ b/examples/bookmarks/tests/test_bookmark_model.cpp @@ -242,12 +242,11 @@ TEST_CASE("GetChangesSince returns only bookmarks touched after the given instan TEST_CASE("GetChangesSince does not miss a write landing in the same millisecond as the cursor", "[bookmarks][model]") { - // Regression test for issue #43: a cursor that compares only on - // updated_at_ms with strict `>` can silently drop a write whose - // timestamp equals the previous poll's asOf (same millisecond -- a - // plausible timing window on a fast machine or a loaded CI runner, not - // a contrived one). Frozen to a single instant, like the analogous - // same-millisecond BulkEdit regression test above, so the race is + // A cursor that compares only on updated_at_ms with strict `>` can silently + // drop a write whose timestamp equals the previous poll's asOf (same + // millisecond -- a plausible timing window on a fast machine or a loaded CI + // runner, not a contrived one). Frozen to a single instant, like the + // analogous same-millisecond BulkEdit regression test above, so the race is // deterministic rather than relying on incidental timing. DbFixture fixture; bookmarks::BookmarkModel model; diff --git a/examples/pastebin/README.md b/examples/pastebin/README.md index 2bf6a4275..5190e2ccc 100644 --- a/examples/pastebin/README.md +++ b/examples/pastebin/README.md @@ -190,9 +190,7 @@ must both work unchanged. the returned column count, but the first `FetchRow()` throws SQLSTATE 24000 "Invalid cursor state"; it never opens a cursor over the returned rows — filed upstream as - [`LASTRADA-Software/Lightweight#545`](https://github.com/LASTRADA-Software/Lightweight/issues/545), - tracked morph-side as - [`LASTRADA-Software/morph#58`](https://github.com/LASTRADA-Software/morph/issues/58). + [`LASTRADA-Software/Lightweight#545`](https://github.com/LASTRADA-Software/Lightweight/issues/545). `PasteModel::execute(const GetPaste&)` therefore runs a `SqlTransaction` around (1) the identical conditional `UPDATE` minus its `RETURNING` clause, dispatched on `NumRowsAffected()`, and (2) an ordinary @@ -402,9 +400,8 @@ the `BridgeHandler` `AppContext::onReady()` hands it. standalone finding. The sqliteodbc `RETURNING`/`SQLFetch` gap (see "Burn-atomicity" above) is filed upstream against [`Lightweight`](https://github.com/LASTRADA-Software/Lightweight/issues/545) - and tracked morph-side as - [`morph#58`](https://github.com/LASTRADA-Software/morph/issues/58) — a - genuine third-party ODBC driver limitation, not fixable in morph source. + and tracked morph-side — a genuine third-party ODBC driver limitation, not + fixable in morph source. - The WASM client's verification status, above. - **`ladder-tests` still builds no GUI.** That job's distro Qt is 6.4.2, below the 6.5 floor `MORPH_BUILD_FORMS_QML` requires, so it configures without the diff --git a/examples/pastebin/gui_lib/paste_qml_bridges.hpp b/examples/pastebin/gui_lib/paste_qml_bridges.hpp index 62b94def1..c5a2e3453 100644 --- a/examples/pastebin/gui_lib/paste_qml_bridges.hpp +++ b/examples/pastebin/gui_lib/paste_qml_bridges.hpp @@ -87,7 +87,7 @@ namespace pastebin::gui { /// The rung's two neighbours are already covered and neither mechanism reaches /// here: `PastePresenter` inherits `Presenter::track()`, which re-checks a /// `QPointer` before touching the presenter (`examples/common/gui/presenter.hpp` -/// — added for a real AddressSanitizer `stack-use-after-scope`, morph#137), and +/// — without it AddressSanitizer reports a `stack-use-after-scope`), and /// `PasteBridge` relays through Qt signal/slot connections, which Qt severs /// when either end is destroyed. `FormsBridge` goes through neither, so it /// takes the framework's general answer: diff --git a/examples/pastebin/include/pastebin/dto/paste_dto.hpp b/examples/pastebin/include/pastebin/dto/paste_dto.hpp index d59d21968..055b9f64c 100644 --- a/examples/pastebin/include/pastebin/dto/paste_dto.hpp +++ b/examples/pastebin/include/pastebin/dto/paste_dto.hpp @@ -94,7 +94,7 @@ struct CreatePaste { "editability"}; /// @brief The burn budget's own rules, declared where the schema can serve - /// them (`morph::forms::FieldMeta`, morph#310). + /// them (`morph::forms::FieldMeta`). /// /// `minimum = 1` and `multipleOf = 1` are the whole of the burn-budget /// rule: at least one read, and a whole number of them. Both are checked diff --git a/examples/pastebin/include/pastebin/models/paste_id_source.hpp b/examples/pastebin/include/pastebin/models/paste_id_source.hpp index 6c18829d0..604cf7062 100644 --- a/examples/pastebin/include/pastebin/models/paste_id_source.hpp +++ b/examples/pastebin/include/pastebin/models/paste_id_source.hpp @@ -17,9 +17,9 @@ /// an ambient dependency like time is, so a test that wants to exercise the /// retry path has only two options without a seam: occupy a share of the /// keyspace and *sample* the resulting collision distribution, or nothing. -/// Sampling is what morph#365 was filed about — the case's outcome then -/// depends on `std::random_device`, which no Catch2 `--rng-seed` reaches, so -/// it can neither be reproduced nor pinned. +/// Sampling is the hazard this replaces — a sampled case's outcome depends on +/// `std::random_device`, which no Catch2 `--rng-seed` reaches, so it can +/// neither be reproduced nor pinned. /// /// A **process-global** provider with a scoped override, rather than a /// constructor parameter, for exactly the reason `clock.hpp` records for diff --git a/examples/pastebin/include/pastebin/units.hpp b/examples/pastebin/include/pastebin/units.hpp index 053fb6b02..ed213695d 100644 --- a/examples/pastebin/include/pastebin/units.hpp +++ b/examples/pastebin/include/pastebin/units.hpp @@ -49,8 +49,8 @@ namespace pastebin { /// `validate()` evaluates that declaration through /// `morph::forms::allFieldBoundsSatisfied`, and `schemaJson()` /// serves the identical two numbers to the client as standard JSON-Schema -/// keys, so the constraint is stated once (morph#310). Every other `Reads` in -/// the rung is *produced* by +/// keys, so the constraint is stated once. Every other `Reads` in the rung is +/// *produced* by /// the model from a whole `std::int64_t` (`paste_model.cpp`'s `readsOf`), so /// `PasteView`'s two `Reads` members are whole by construction rather than by /// validation, and `EditPaste` carries no read count at all. diff --git a/examples/pastebin/src/db/schema.cpp b/examples/pastebin/src/db/schema.cpp index 0afbc2484..992e0b50b 100644 --- a/examples/pastebin/src/db/schema.cpp +++ b/examples/pastebin/src/db/schema.cpp @@ -9,12 +9,12 @@ namespace pastebin::db { void setup(const std::string& connectionString) { - // morph#740: nothing in Lightweight stops a pooled DataMapper from being - // returned with a transaction still open on it -- `DataMapperPool::Return` - // does no transaction cleanup, and the cost lands on the next, unrelated - // borrower as a 60s stall and a `database is locked` it did not cause. - // Installing the audit here, at the one point every rung's process - // configures its database, turns that into an abort at the leak. See + // Nothing in Lightweight stops a pooled DataMapper from being returned with + // a transaction still open on it -- `DataMapperPool::Return` does no + // transaction cleanup, and the cost lands on the next, unrelated borrower + // as a 60s stall and a `database is locked` it did not cause. Installing + // the audit here, at the one point every rung's process configures its + // database, turns that into an abort at the leak. See // examples/common/db/pool_transaction_audit.hpp. (void)::morph::ladder::db::installPoolTransactionAudit(); diff --git a/examples/pastebin/src/models/paste_model.cpp b/examples/pastebin/src/models/paste_model.cpp index d5d9ab958..2bfaf91e9 100644 --- a/examples/pastebin/src/models/paste_model.cpp +++ b/examples/pastebin/src/models/paste_model.cpp @@ -131,7 +131,7 @@ namespace { /// *every* id this generator can spell and require `CreatePaste` to /// give up. (The collision-retry test no longer depends on the size: /// it scripts the collisions through `pastebin::PasteIdSource` rather -/// than sampling them — see morph#365.) +/// than sampling them.) constexpr std::array kAnimals = { "cat", "dog", "fox", "owl", "bee", "ant", "elk", "ram", "yak", "cod", "eel", "hen", "pig", "cow", "bat", "jay", }; diff --git a/examples/pastebin/tests/test_paste_model.cpp b/examples/pastebin/tests/test_paste_model.cpp index a865f47e9..29f9206d7 100644 --- a/examples/pastebin/tests/test_paste_model.cpp +++ b/examples/pastebin/tests/test_paste_model.cpp @@ -495,12 +495,11 @@ TEST_CASE("CreatePaste's validate() rejects a fractional burnAfterReads", "[past TEST_CASE("The burn-budget rules reach the served CreatePaste schema", "[pastebin][model][forms]") { // `IMPLEMENTATION.md` rule 3: the DTO *is* the form definition, and there - // is no second source of truth. Until morph#310 there was one here — the - // three conditions above (>= 1, non-negative, whole) lived only in - // `validate()`, server-side, with nothing a client could gate on. The form - // therefore auto-fired into a guaranteed rejection, and the only remaining - // way to stop it was a hand-written QML conditional, which - // `examples/TESTING.md` presenter rule 6 forbids. + // is no second source of truth. With the three conditions above (>= 1, + // non-negative, whole) living only in `validate()`, server-side, there is + // nothing a client can gate on: the form auto-fires into a guaranteed + // rejection, and the only remaining way to stop it is a hand-written QML + // conditional, which `examples/TESTING.md` presenter rule 6 forbids. // // `FieldMeta::minimum`/`::multipleOf` are the vocabulary that closes it: // one declaration on the DTO, served as standard JSON-Schema keys and @@ -1528,13 +1527,13 @@ TEST_CASE("CreatePaste retries past colliding animal-name ids instead of failing // if the retry logic itself is wrong. // // The previous version occupied a quarter of the keyspace and let the real - // generator roll (morph#365). That made an eight-attempt exhaustion a - // 1-in-1,640 event across its 40 creates — a red build on an unrelated PR - // that often, unreproducible when it fired, because `randomPasteId()` - // seeds from `std::random_device` and Catch2's `--rng-seed` cannot reach - // it. Its odds were the thing under test; here the retry *logic* is. The - // collisions are a real primary-key violation from the real store either - // way — the seam supplies the candidate id, not the verdict on it. + // generator roll. That made an eight-attempt exhaustion a 1-in-1,640 event + // across its 40 creates — a red build on an unrelated PR that often, + // unreproducible when it fired, because `randomPasteId()` seeds from + // `std::random_device` and Catch2's `--rng-seed` cannot reach it. Its odds + // were the thing under test; here the retry *logic* is. The collisions are + // a real primary-key violation from the real store either way — the seam + // supplies the candidate id, not the verdict on it. DbFixture fixture; pastebin::PasteModel model; @@ -1878,8 +1877,7 @@ TEST_CASE("db::setup points the default connection at a database and applies the TEST_CASE("App teardown survives sweep dispatches still outstanding (the member-order guard)", "[pastebin][app]") { // The guard for `App`'s member declaration order, which is the entire // protection against a `post()` on a freed `QtExecutor` - // (`pastebin/app/app.hpp`'s comment above `_sweepExecutor`, and morph#127, - // which was a shipped bug of exactly that class). `_sweepExecutor` is + // (`pastebin/app/app.hpp`'s comment above `_sweepExecutor`). `_sweepExecutor` is // declared *first*, so it is destroyed *last* — after `~_pool` has joined // its worker threads. Move it to its natural reading position at the end // of the member list and this case segfaults deterministically: diff --git a/examples/polls/README.md b/examples/polls/README.md index 0b5b20d2a..ff82bec41 100644 --- a/examples/polls/README.md +++ b/examples/polls/README.md @@ -18,9 +18,9 @@ here, in writing, before any task starts — the discipline rule this ladder runs on. 1. **`session::Principal` is not a capability-token mechanism — correction.** - This README originally described participant identity as "the participant - token in `session::Context` ... `session::Principal` (added in #34) - carrying a capability token instead of a user identity." The real + Participant identity is *not* "the participant token in + `session::Context` ... `session::Principal` carrying a capability token + instead of a user identity", which is the easy misreading. The real `session::Principal` (`docs/spec/session/session.md`) is a client-side, `Bridge`-scoped UI cache populated *after* login from server-returned data — it has no wire representation and does not participate in @@ -92,8 +92,8 @@ runs on. 4. **`GetEventsSince` is genuinely new work, not a `GetChangesSince` port.** Rung 2's `GetChangesSince` is a timestamp-diffed-current-state view (`WHERE updatedAtMs > since OR (updatedAtMs = since AND id > lastId)` — - the `id` tie-break is issue #43's fix for the millisecond-boundary case a - bare `updatedAtMs > since` can silently drop; still a current-state view, + the `id` tie-break covers the millisecond-boundary case a bare + `updatedAtMs > since` silently drops; still a current-state view, returning full current rows, not a log) — not the Zulip append-only event-log pattern this rung's own "morph subsystems exercised" section correctly calls for. **Resolved shape**: a genuine `poll_events` table @@ -122,11 +122,11 @@ runs on. is a real, shipped, separately-tested per-connection token bucket. A frame that finds an empty bucket is **refused, and answered**: it never reaches `RemoteServer`, and the sender receives an `err "rate limited"` addressed - to that frame's own `callId` (morph#225 — - `src/qt/qt_websocket_server.cpp`'s `makeErr("rate limited", …)` call, and - `include/morph/qt/qt_websocket_server.hpp`'s own doc comment). It used to - be dropped silently; that is what made a rate-limited call hang until the - client-side deadline fired, and it is no longer the case. This rung's own + to that frame's own `callId` (`src/qt/qt_websocket_server.cpp`'s + `makeErr("rate limited", …)` call, and + `include/morph/qt/qt_websocket_server.hpp`'s own doc comment). Dropping it + silently is what would make a rate-limited call hang until the client-side + deadline fired. This rung's own "run this rung's harness with `messagesPerSecond` configured ON" is a **test-harness configuration decision**, not new framework work — the client-side execute-deadline prerequisite below is what actually needs @@ -147,10 +147,9 @@ runs on. `IBackend::promoteModel` (`include/morph/core/backend.hpp`), `QtWebSocketBackend::promoteModel` (`src/qt/qt_websocket_backend.cpp`), and `assignHandlerPrimary` itself, - which calls it unconditionally (`include/morph/core/bridge.hpp`). When - the claim was written that layer was an optional non-blocking twin a - backend could decline; morph#567–morph#571 replaced it with - `promoteModel`, which no backend can decline. + which calls it unconditionally (`include/morph/core/bridge.hpp`). It is + not an optional non-blocking twin a backend can decline: `promoteModel` + is the one verb, and no backend can decline it. - **The promote step never runs for this rung anyway.** `assignHandlerPrimary` is reached from exactly one branch of `BridgeHandler::execute`, guarded by `kShared && @@ -196,9 +195,8 @@ runs on. back from that raw value. `ModelKey` is the union of the two. `lims` keys `SampleModel` on `lims::SampleId` through exactly that path (`BRIDGE_MODEL_KEY(lims::SampleModel, lims::OpenSample, - &lims::OpenSample::sampleId)`), and the widening (morph#163) let - morph#183 delete three rungs' hand-written `ModelKeyTraits` - specialisations. + &lims::OpenSample::sampleId)`), and the widening is what lets three rungs + carry no hand-written `ModelKeyTraits` specialisations at all. **Current state, stated as such:** `polls::OpenPoll::pollId` is still a plain `std::string`, and this rung has not been migrated to a @@ -229,18 +227,17 @@ each now lives: WASM main thread, so a WASM client must not reach it — and with `Config::asyncRegistrationEnabled` set it does not, so the very first `OpenPoll` a WASM tab makes is no longer blocked on the framework. Built as - this rung's first framework-level task, and built in the shape that existed - then: a pair of optional non-blocking twins a backend returned `true` or - `false` from, with the caller falling back to the synchronous verb on - `false`. morph#567–morph#571 removed the twins in favour of the one verb - above, so there is no opt-in left to decline and no fallback path; the - synchronous verbs survive only as what the *default* `bindModel` runs. + this rung's first framework-level task. Not a pair of optional non-blocking + twins a backend returns `true` or `false` from with the caller falling back to + the synchronous verb on `false`: there is one verb, so there is no opt-in to + decline and no fallback path, and the synchronous verbs survive only as what + the *default* `bindModel` runs. - **Client-side execute deadline.** *Shipped:* `Bridge::setExecuteDeadline` (`include/morph/core/bridge.hpp`), specified in `docs/spec/core/completion.md`. Without it a genuinely hung server blocked the calling `Completion` forever. (A frame refused by `messagesPerSecond` - used to belong here too; it no longer does, since the transport now answers - it with an `err "rate limited"` — morph#225.) + does *not* belong here: the transport answers it with an + `err "rate limited"`.) `Completion::state()` already exposes the underlying `CompletionState`, and `CompletionState::setException` is idempotent-guarded (`if (ready) return;`), so the fix needs no @@ -358,7 +355,7 @@ log table above. call against a hung server hangs its completion forever, which is what `Bridge::setExecuteDeadline` now exists to bound. A rate-limited server no longer drops frames silently — it answers them with `err "rate limited"` - (morph#225) — so the limiter is no longer the case that motivates the + — so the limiter is no longer the case that motivates the timeout; a genuinely unresponsive server is. **Done:** this rung's harness does run with `messagesPerSecond` configured ON (a polling app is the abuse case the limiter exists for), and @@ -520,7 +517,7 @@ Known gaps: the real handler's `Sharing` argument, so `kShared` resolved `false` at that call site no matter what, and dispatching a payload-keyed action through `executeJson` on an `AllowShared` handler silently never attached. - **It was fixed framework-side (morph#68)**: `registerAction` now builds one + **It was fixed framework-side**: `registerAction` now builds one executor per `Sharing` policy from the same generic-lambda template, keyed by `(modelId, actionId, typeid(Sharing))`, and `executeJson` dispatches through the handler's own real policy — `ActionExecuteRegistry` and the diff --git a/examples/polls/gui_lib/poll_qml_bridges.cpp b/examples/polls/gui_lib/poll_qml_bridges.cpp index 39884d598..ec71a1f55 100644 --- a/examples/polls/gui_lib/poll_qml_bridges.cpp +++ b/examples/polls/gui_lib/poll_qml_bridges.cpp @@ -78,7 +78,7 @@ using ::morph::ladder::gui::idNumber; // from toLongLong(), which is exactly the "not entered" state the // action's validate() is there to reject -- a clean ValidationError, // rather than the exception fromRowId raises for a corrupt *stored* - // id (morph#215). + // id. out.push_back(OneVote{.optionId = OptionId{.value = row.value(QStringLiteral("optionId")).toLongLong()}, .choice = parseChoice(row.value(QStringLiteral("choice")).toString())}); } diff --git a/examples/polls/include/polls/core/types.hpp b/examples/polls/include/polls/core/types.hpp index 5145b11e4..dbd5d96d8 100644 --- a/examples/polls/include/polls/core/types.hpp +++ b/examples/polls/include/polls/core/types.hpp @@ -24,8 +24,7 @@ /// the beginning" for `GetEventsSince` -- but it carries a constraint the /// type cannot enforce on its own: **an id of `0` is unrepresentable**. /// Construct one and it reports `hasValue() == false` and behaves as absent -/// everywhere downstream, so a real record would read as "no record" -/// (morph#215). +/// everywhere downstream, so a real record would read as "no record". /// /// The constraint holds because both ids come from SQLite row ids, which /// start at 1. `fromRowId()` is the enforcement: every conversion from a @@ -72,11 +71,11 @@ struct OptionId { /// represent. /// /// `0` is this type's "not entered" sentinel, so an id of `0` would arrive - /// as *absent* and a real option would read as "no option selected" - /// (morph#215). SQLite row ids start at 1, so this never fires in - /// practice -- it exists so that a seeded row, a migrated dataset, an - /// externally supplied key, or a sequence reset fails loudly at the - /// boundary instead of collapsing silently one layer below the surface. + /// as *absent* and a real option would read as "no option selected". + /// SQLite row ids start at 1, so this never fires in practice -- it exists + /// so that a seeded row, a migrated dataset, an externally supplied key, or + /// a sequence reset fails loudly at the boundary instead of collapsing + /// silently one layer below the surface. /// @param rowId Stored row id; must be non-zero. /// @return An engaged `OptionId` wrapping @p rowId. /// @throws PollsError if @p rowId is `0`. @@ -84,7 +83,7 @@ struct OptionId { if (rowId == 0) { throw PollsError{ "OptionId::fromRowId: an option row id of 0 is unrepresentable -- 0 is this " - "type's \"not entered\" sentinel (morph#215)"}; + "type's \"not entered\" sentinel"}; } return OptionId{.value = rowId}; } @@ -121,10 +120,10 @@ struct PollEventId { /// `0` is this type's "not entered" sentinel -- the spelling /// `GetEventsSince` uses for "no cursor yet, replay from the beginning" -- /// so an event row id of `0` would arrive as *absent* and the reader would - /// silently rewind to the start of the log (morph#215). SQLite row ids - /// start at 1, so this never fires in practice; it exists so that a - /// seeded row, a migrated dataset, or a sequence reset fails loudly at the - /// boundary rather than collapsing silently. + /// silently rewind to the start of the log. SQLite row ids start at 1, so + /// this never fires in practice; it exists so that a seeded row, a migrated + /// dataset, or a sequence reset fails loudly at the boundary rather than + /// collapsing silently. /// @param rowId Stored row id; must be non-zero. /// @return An engaged `PollEventId` wrapping @p rowId. /// @throws PollsError if @p rowId is `0`. @@ -132,7 +131,7 @@ struct PollEventId { if (rowId == 0) { throw PollsError{ "PollEventId::fromRowId: an event row id of 0 is unrepresentable -- 0 is this " - "type's \"not entered\" sentinel (morph#215)"}; + "type's \"not entered\" sentinel"}; } return PollEventId{.value = rowId}; } diff --git a/examples/polls/src/db/schema.cpp b/examples/polls/src/db/schema.cpp index ed0077f85..047428878 100644 --- a/examples/polls/src/db/schema.cpp +++ b/examples/polls/src/db/schema.cpp @@ -9,12 +9,12 @@ namespace polls::db { void setup(const std::string& connectionString) { - // morph#740: nothing in Lightweight stops a pooled DataMapper from being - // returned with a transaction still open on it -- `DataMapperPool::Return` - // does no transaction cleanup, and the cost lands on the next, unrelated - // borrower as a 60s stall and a `database is locked` it did not cause. - // Installing the audit here, at the one point every rung's process - // configures its database, turns that into an abort at the leak. See + // Nothing in Lightweight stops a pooled DataMapper from being returned with + // a transaction still open on it -- `DataMapperPool::Return` does no + // transaction cleanup, and the cost lands on the next, unrelated borrower + // as a 60s stall and a `database is locked` it did not cause. Installing + // the audit here, at the one point every rung's process configures its + // database, turns that into an abort at the leak. See // examples/common/db/pool_transaction_audit.hpp. (void)::morph::ladder::db::installPoolTransactionAudit(); diff --git a/examples/polls/tests/test_polls_types.cpp b/examples/polls/tests/test_polls_types.cpp index be8c40515..abcd92836 100644 --- a/examples/polls/tests/test_polls_types.cpp +++ b/examples/polls/tests/test_polls_types.cpp @@ -29,7 +29,7 @@ TEST_CASE("PollsError hierarchy: each derived type carries its own message", "[p TEST_CASE("polls id types: fromRowId rejects the one value they cannot represent", "[polls][types]") { // 0 is these types' "not entered" sentinel, so an id of 0 would arrive as - // *absent* and a real record would read as "no record" (morph#215). Row + // *absent* and a real record would read as "no record". Row // ids start at 1, so this never fires in practice -- the point is that a // seeded, migrated, or externally supplied 0 fails loudly at the boundary // instead of collapsing silently one layer below the QML surface. diff --git a/examples/polls/tests/test_shared_instance_lifecycle.cpp b/examples/polls/tests/test_shared_instance_lifecycle.cpp index 6bc252e99..cad9dcf27 100644 --- a/examples/polls/tests/test_shared_instance_lifecycle.cpp +++ b/examples/polls/tests/test_shared_instance_lifecycle.cpp @@ -47,11 +47,10 @@ // refuses an over-budget call without hanging it -- the DoD's "run this // rung's harness with messagesPerSecond configured ON" requirement, proven // end to end (not merely at the framework-prereqs plan's own unit-test -// level) for the first time in this rung. This case used to prove that -// setExecuteDeadline *recovered* such a call, because the transport dropped -// the frame silently and only the deadline could settle it; since morph#225 -// the transport answers it, so the call settles on its own and the deadline -// is no longer what saves it. +// level) for the first time in this rung. What it does *not* prove is that +// setExecuteDeadline recovers such a call: the transport answers the refused +// frame, so the call settles on its own. A transport that dropped it +// silently would leave the deadline as the only thing that could. // 3. The cross-model rename-race analogue (rung 2's TagModel-renames-while- // BookmarkModel-writes race): this rung's README does not name an exact // analogue -- there is only one model type here (PollModel), so that @@ -293,12 +292,12 @@ TEST_CASE("The real rate limiter refuses an over-budget call without hanging it" // messagesPerSecond, one token per incoming frame of any kind, refilling // continuously; a frame that finds an empty bucket is refused -- it never // reaches RemoteServer, and the sender is answered with an - // `err "rate limited"` addressed to that frame's own callId (morph#225) - // (mirrors tests/qt/test_qt_websocket.cpp's own - // "messagesPerSecond throttles a burst on one connection" construction - // pattern -- ThreadPoolExecutor -> RemoteServer -> QtWebSocketServer with - // a low-messagesPerSecond cfg -- except BackendRig already threads that - // cfg straight through, so no hand-built server is needed here). + // `err "rate limited"` addressed to that frame's own callId (mirrors + // tests/qt/test_qt_websocket.cpp's own "messagesPerSecond throttles a burst + // on one connection" construction pattern -- ThreadPoolExecutor -> + // RemoteServer -> QtWebSocketServer with a low-messagesPerSecond cfg -- + // except BackendRig already threads that cfg straight through, so no + // hand-built server is needed here). DbFixture fixture; ::morph::qt::QtWebSocketServerConfig cfg; cfg.messagesPerSecond = 5; // bucket capacity 5, refills at 5/s -- same @@ -372,9 +371,9 @@ TEST_CASE("The real rate limiter refuses an over-budget call without hanging it" CHECK(rateLimited >= 1); // The deadline is armed above and is deliberately *not* what settles these - // calls any more: before morph#225 a refused frame was dropped silently and - // only setExecuteDeadline could end the wait, so this case asserted - // clientTimeouts >= 1. Now the transport answers, so a timeout here would - // mean a call really did go unanswered -- the regression this guards. + // calls: the transport answers a refused frame, so a timeout here would mean + // a call really did go unanswered -- the regression this guards. Were + // refused frames dropped silently, only setExecuteDeadline could end the + // wait and this would have to assert clientTimeouts >= 1 instead. CHECK(clientTimeouts == 0); } From e4c4f29202579735a9b912ef4c497dca08b2e556 Mon Sep 17 00:00:00 2001 From: Yaraslau Tamashevich Date: Thu, 24 Sep 2026 03:24:19 +0200 Subject: [PATCH 09/10] =?UTF-8?q?comments(examples):=20state=20the=20const?= =?UTF-8?q?raint,=20drop=20the=20citation=20=E2=80=94=20the=20ladder's=20o?= =?UTF-8?q?wn=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit IMPLEMENTATION.md, TESTING.md, LADDER.md, rungs.txt, examples/CMakeLists.txt and the three non-rung trees (forms, concepts, qt_tls_client). TESTING.md keeps every measurement it rests on -- the 165 TSan warnings that make a Qt-linked rung unsanitisable, the 95.85% `examples/common` baseline and the four-file split that accounts for the drop since, the `ctest -L ladder-lims` 85-vs-87 count. What goes is the tickets and the "was documented long before anything enforced it" framing: a path filter that matches nothing succeeds as loudly as one with nothing to do whether or not that has happened yet, and that is the sentence worth keeping. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW --- examples/CMakeLists.txt | 4 +- examples/IMPLEMENTATION.md | 7 ++-- examples/LADDER.md | 18 ++++---- examples/TESTING.md | 60 +++++++++++++-------------- examples/concepts/CMakeLists.txt | 2 +- examples/forms/lab_model.hpp | 6 +-- examples/qt_tls_client/CMakeLists.txt | 5 ++- examples/rungs.txt | 2 +- 8 files changed, 50 insertions(+), 54 deletions(-) diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index c110e46c6..075c29945 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -32,8 +32,8 @@ add_subdirectory(common) # examples/rungs.txt, which CI's path-filter steps and scripts/coverage.sh also # read — those run on a bare checkout, before any configure, so a CMake # variable is unreachable to them. Hand-copying the list into each of them is -# what produced morph#179: ci.yml's filter had stopped at kanban, so rungs 5 -# and 6 matched nothing and skipped the ladder jobs entirely. +# what one stale copy costs: a ci.yml path filter that stops at an earlier rung +# matches nothing for the later ones and skips the ladder jobs entirely. set(_morph_rung_file "${CMAKE_CURRENT_SOURCE_DIR}/rungs.txt") set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${_morph_rung_file}") if(NOT EXISTS "${_morph_rung_file}") diff --git a/examples/IMPLEMENTATION.md b/examples/IMPLEMENTATION.md index e1f506f6f..cd7b13629 100644 --- a/examples/IMPLEMENTATION.md +++ b/examples/IMPLEMENTATION.md @@ -45,9 +45,8 @@ The user-code contract is: **you implement Models; morph exposes them.** *"the backend is unreachable — queue this action instead of sending it"* is app-layer by design and sits at the dispatch site, outside any model. This is not an exemption from the rule; it is the rule's last clause - having fired. The finding was raised - ([morph#197](https://github.com/LASTRADA-Software/morph/issues/197)) and - dispositioned as app-layer under the promotion rule above; the reasoning, + having fired. It was raised as a finding and dispositioned as app-layer + under the promotion rule above; the reasoning, the boundary, and the reference shapes are in [`docs/spec/offline/offline.md`](../docs/spec/offline/offline.md) ("Ownership: who enqueues" → "Disposition: app-layer by design"). The @@ -119,7 +118,7 @@ content, URLs). Everything else is a strong type: | Points in time | `morph::time::Timestamp` / `DateTime` | | Foreign keys / lookups chosen by a user | `morph::forms::Choice` | | Entity identity | A per-entity strong id type (e.g. `struct PasteId`) exposing `hasValue()` so it joins the forms palette as an empty-capable field | -| Closed sets of states/options | `enum class` **with a `glz::meta`/`glz::enumerate` specialisation** (never a bare integer, never `bool` — a two-state flag is a two-enumerator `enum class`, per the readability rule that call sites must not read `f(true)`). The `glz::meta` is not optional decoration: `morph::forms::schemaJson()` refuses to compile without one on any `enum class` member it reaches (morph#392) — glaze cannot describe the closed set otherwise, and a renderer would draw a checkbox for it instead of a combo box. | +| Closed sets of states/options | `enum class` **with a `glz::meta`/`glz::enumerate` specialisation** (never a bare integer, never `bool` — a two-state flag is a two-enumerator `enum class`, per the readability rule that call sites must not read `f(true)`). The `glz::meta` is not optional decoration: `morph::forms::schemaJson()` refuses to compile without one on any `enum class` member it reaches — glaze cannot describe the closed set otherwise, and a renderer would draw a checkbox for it instead of a combo box. | | Optional fields | empty-capable state (`hasValue()` / empty `Quantity`) or the action's `optionalFields` opt-out — not `std::optional`, which silently loses schema annotations (see the round-5 review finding in [`LADDER.md`](LADDER.md)) | | Line items / sub-objects | nested aggregates of the same palette | | Protocol scalars — pagination cursors, event ids / epoch tokens, op-ids / idempotency keys, base versions, job ids, capability & confirmation tokens | A named opaque newtype per role (e.g. `struct EventId`, `struct Cursor`), `hasValue()`-capable, serialising as its underlying scalar — **never** a bare `int64_t` and never a loose `std::string`. If morph offers no cheap `Tagged` helper that joins glaze and the forms palette, that is a **day-one finding filed once**, not eight hand-rolled wrapper sets (round-7 T2). | diff --git a/examples/LADDER.md b/examples/LADDER.md index 50e928712..cac46052f 100644 --- a/examples/LADDER.md +++ b/examples/LADDER.md @@ -110,12 +110,10 @@ audit). [`bank/README.md`](bank/README.md) sets out which conventions it shares and which it does not, and carries the two warnings that go with the server: it authenticates nobody — it trusts whatever principal a client asserts, because bank's `AuthModel` mints no token — and its corpus pins -several known defects as `expect ok` rather than hiding them, chief among -them -[morph#471](https://github.com/LASTRADA-Software/morph/issues/471). Bank's -relationship to the ladder is -[morph#87](https://github.com/LASTRADA-Software/morph/issues/87); the rest of -that issue — bringing bank's *conventions* into line — is open. +several known defects as `expect ok` rather than hiding them, each named in +that corpus. Bank's relationship to the ladder is settled — it sits outside the +numbered sequence, see the table below — while bringing bank's *conventions* +into line is open work. ## Cross-cutting stress map @@ -255,10 +253,10 @@ check rather than take this section's word for it: one non-blocking acquire verb whose *request shape* selects private registration, register-or-attach or re-point — consumed by `Bridge::ensureBoundAsync`/`attachHandlerAsync` and implemented natively by - `QtWebSocketBackend`. It first shipped as a pair of optional non-blocking - twins beside the synchronous verbs; morph#567–morph#571 removed the twins - in favour of the single surface above, under which the synchronous verbs - survive only as what `IBackend`'s *default* `bindModel` dispatches to. + `QtWebSocketBackend`. Not a pair of optional non-blocking twins beside the + synchronous verbs: there is the single surface above, under which the + synchronous verbs survive only as what `IBackend`'s *default* `bindModel` + dispatches to. 2. **Client-side execute deadline** (before rung 3's polling helper) — no timeout existed on a `Completion`, so a black-holed server hung the client forever. **Shipped:** `Bridge::setExecuteDeadline` diff --git a/examples/TESTING.md b/examples/TESTING.md index cae620f18..e3f807901 100644 --- a/examples/TESTING.md +++ b/examples/TESTING.md @@ -145,8 +145,8 @@ deleted, an alias nobody bound, or a member QML does bind is a finding — so it can only shrink deliberately. Adopted by every rung that has QML — `bookmarks`, `pastebin`, `polls`, -`ledger`, `lims` and `kanban` — and by `examples/bank`, which is not a rung -(morph#240). The audit's own mutation suite is +`ledger`, `lims` and `kanban` — and by `examples/bank`, which is not a rung. +The audit's own mutation suite is `examples/common/testkit/test_qml_surface.cpp`: every case there drives it against a deliberately broken pair and asserts the specific finding. @@ -172,7 +172,7 @@ configure. and the reason is worth stating because no rung needs it. The audit proves a name written in a `.qml` file resolves against the metaobject; it cannot prove the binding behaves. `MoveMoneyPage.qml`'s account picker was the case that -forced the distinction (morph#296): the controller was self-consistent under +forced the distinction: the controller was self-consistent under every C++ drive, and the defect was a `ComboBox` whose `currentIndex` nothing restored after its `model` was replaced. So `bank_gui_qml_tests` loads the shipped `.qml` from the source tree by URL — the `BankGui` QML module lives @@ -256,9 +256,9 @@ DeferredDelete drain) copied from `tests/qt/test_qt_websocket.cpp`. The one sanctioned exception is a test that must not link Qt at all. `examples/kanban/tests/test_kanban_stress.cpp` is the case: it runs under -ThreadSanitizer, and morph#128 established that routing its callbacks through -a `QtExecutor` produced 165 TSan warnings that all bottomed out in Qt-internal -frames a prebuilt Qt makes unreadable — evidence for nothing either way. It +ThreadSanitizer, and routing its callbacks through a `QtExecutor` produces 165 +TSan warnings that all bottom out in Qt-internal frames a prebuilt Qt makes +unreadable — evidence for nothing either way. It therefore owns a small `waitUntil` over `sleep_for` instead of `pumpUntil`. Such a loop still owes the scaling: `examples/common/testkit/deadline.hpp` holds `computeDeadlineScale`/`deadlineScale` with **no Qt dependency**, @@ -290,7 +290,7 @@ and pays the retry loop knowingly. The better answer, where the design allows it, is to have no background worker under the test at all. `examples/ledger/tests/test_ledger_reports.cpp` -takes that route: since morph#160 the report aggregation is an ordinary +takes that route: the report aggregation is an ordinary `RunReportJob` action rather than a task posted to an executor the model owns, so the file needs neither a retry loop nor a `StepExecutor` — "the report has been computed" is what the dispatch returning means, and "nothing @@ -544,7 +544,7 @@ Open framework facts every rung must respect (verified): **`asyncRegistrationEnabled = true`, which is opt-in and off by default**; with defaults, the first `registerModel` aborts the page. - **`waitForConnected()` hangs the page on WASM** — the WASM client must - use the `setConnectHandler` pattern (#39) instead; the Socket rig's + use the `setConnectHandler` pattern instead; the Socket rig's `waitForConnected()` recipe is for *native* tests only. - The **synchronous shared/keyed attach path (`registerModelShared`/`attachModel`) nests an event loop that aborts the @@ -569,17 +569,15 @@ root `CMakeLists.txt` — don't repeat that eight times): `MORPH_LADDER_RUNGS` cache list (`"all"` or `"pastebin;kanban"`) — no per-rung booleans; the list maps 1:1 to CI path filters. - **The rung names themselves live in `examples/rungs.txt`, and nowhere - else.** That invariant above — "maps 1:1 to CI path filters" — was - documented long before anything enforced it, and it did not hold: the rung - list was hand-copied into five places, and every copy that was not - load-bearing eventually drifted. CI's `ladder-tests`/`ladder-sanitizers` - path filter stopped at `kanban` (rung 4), so a change confined to - `examples/ledger/` or `examples/lims/` matched nothing and skipped both - jobs — including the only job in the repository that sanitizer-instruments - a rung. Nothing reported it: a path filter that matches nothing succeeds - exactly as loudly as one that correctly found nothing to do (morph#179; - `scripts/coverage.sh` and `codecov.yml` had drifted the same way in - morph#141). The list is now structured so it cannot: + else.** The invariant above — "maps 1:1 to CI path filters" — cannot be + held by documentation alone. Hand-copied into five places, every copy that is + not load-bearing drifts: a `ladder-tests`/`ladder-sanitizers` path filter that + stops at `kanban` (rung 4) makes a change confined to `examples/ledger/` or + `examples/lims/` match nothing and skip both jobs — including the only job in + the repository that sanitizer-instruments a rung. Nothing reports it, because + a path filter that matches nothing succeeds exactly as loudly as one that + correctly found nothing to do; `scripts/coverage.sh` and `codecov.yml` drift + the same way. The list is therefore structured so it cannot: - `examples/rungs.txt` is the single authority: one bare rung name per line, ASCII, whole-line `#` comments. A line that is neither is a hard @@ -650,9 +648,9 @@ root `CMakeLists.txt` — don't repeat that eight times): are not translated into ctest labels anywhere in this repo; select on them with `ctest -R` against the test name instead), a `TEST_PREFIX` of `.` on every discovered ctest name (a ctest name is global to the - build tree, so two rungs sharing a `TEST_CASE` name used to give - `ctest -L ladder-` another rung's cases as well as its own — - morph#464; `scripts/check_ctest_name_collisions.sh` is the gate that keeps + build tree, so without the prefix two rungs sharing a `TEST_CASE` name give + `ctest -L ladder-` another rung's cases as well as its own; + `scripts/check_ctest_name_collisions.sh` is the gate that keeps the names distinct, and note the prefix reaches the *ctest* entry only, so `ctest -R` still matches the bare test name), warnings and sanitizers **applied to all app code** (bank skips both repo-wide because @@ -742,8 +740,8 @@ root `CMakeLists.txt` — don't repeat that eight times): drive Qt on every path, and against an uninstrumented system Qt that yields warnings bottoming out in Qt-internal frames that cannot be classified as real races or false positives from outside a - TSan-instrumented Qt build — morph#128 hit exactly that, 165 warnings - deep. Thread-sanitising a rung therefore means writing a test that + TSan-instrumented Qt build — measured at 165 warnings deep on + `examples/kanban`. Thread-sanitising a rung therefore means writing a test that constructs no `QtExecutor` at all — driving the model through a bare `morph::bridge::Bridge`/`morph::backend::LocalBackend` on a real `morph::exec::ThreadPoolExecutor` — and running just that test under @@ -798,7 +796,7 @@ the deliberately pinned `kanban` in `kanban-tsan`. Per-rung scoping (`examples//**` → that rung; `examples/common/**` or `include/morph/**` → all rungs) is a plausible next step, and was described -here as though it already existed, but it has never been built (morph#255): +here as though it already existed, but it has never been built: 1. **CI (every push/PR)**: one `ladder-tests` job (clone of `linux-qt`: gcc-debug, offscreen, sccache), gated by the boolean changed-paths filter @@ -847,7 +845,7 @@ here as though it already existed, but it has never been built (morph#255): `codecov.yml`'s `ladder` component scores `examples/common/**` against a 95% target. That number was reported as 90.39% on `master` and read as a five-point -regression (morph#411). It is a real figure, it is reproducible, and the +regression. It is a real figure, it is reproducible, and the regression is real — but two things about it have to be said before the figure means anything, because both of them were being read the other way. @@ -877,14 +875,14 @@ A prior local measurement of this component disagreed with the uploaded one by five points for a *different* reason — a shared compiler cache served objects built in another worktree, whose absolute paths matched none of `scripts/coverage.sh`'s relative filters, so the records were dropped rather -than mis-attributed (morph#426). That is fixed at configure time and gated by +than mis-attributed. That is fixed at configure time and gated by `scripts/check_coverage_roots.sh`; run it before trusting any local figure. ### Where the drop came from: four files that did not exist at the measurement -`codecov.yml` records `examples/common` at 95.85% when rungs 2-4 entered the -report (morph#142, 2026-08-21). Splitting today's figure by whether a file -existed then answers the question that ticket asked and could not answer: +`codecov.yml` records `examples/common` at 95.85%, measured when rungs 2-4 +entered the report. Splitting today's figure by whether a file existed then is +what makes the drop since attributable: | | hits | miss | partial | lines | Codecov | |------------------------------------|-----:|-----:|--------:|------:|--------:| @@ -968,7 +966,7 @@ taken on trust. helper no longer wraps its own timer. (A rate-limited frame no longer belongs on this list for a second reason: the transport answers it with an `err "rate limited"` addressed to the frame's own `callId`, so the caller's - `Completion` fails rather than hanging — morph#225.) + `Completion` fails rather than hanging.) 2. **`Bridge::pendingCalls()`** → `include/morph/core/bridge.hpp`, so `settle()` can be exact rather than substituting presenter-level counters. 3. **`MainThreadExecutor::runOnce()`/`drain()`** → diff --git a/examples/concepts/CMakeLists.txt b/examples/concepts/CMakeLists.txt index a05b1cc0e..d7e8c2731 100644 --- a/examples/concepts/CMakeLists.txt +++ b/examples/concepts/CMakeLists.txt @@ -46,7 +46,7 @@ if(MORPH_BUILD_TESTS) # 8 TUs driving include/morph's journal, offline-queue, validation, # transport-limit, versioning, connection-scope, observability and # shutdown paths -- ctest-registered, so every sanitizer leg ran them - # and learned nothing from them (morph#542). + # and learned nothing from them. if(DEFINED AF_SANITIZER) apply_sanitizers(morph_concepts_tests ${AF_SANITIZER}) endif() diff --git a/examples/forms/lab_model.hpp b/examples/forms/lab_model.hpp index da9d12ea2..41165b9d7 100644 --- a/examples/forms/lab_model.hpp +++ b/examples/forms/lab_model.hpp @@ -316,9 +316,9 @@ class LabModel { // numeric half comes from `toDecimalString` rather than from `{:.1f}` // on the dereferenced `Rational`: that spec delegates to // `std::formatter` through `toDouble()` and so leaves the exact - // domain the type exists to stay inside. The decimals shown are now the - // value's own runtime `DecimalPlaces`, trimmed, instead of a hard-coded - // one. See morph#199. + // domain the type exists to stay inside. The decimals shown are the + // value's own runtime `DecimalPlaces`, trimmed, rather than a hard-coded + // count. auto summary = std::format("sample {} at {}: density {} {}", *action.sampleId, *action.measuredAt, morph::units::toDecimalString(action.density), Density::unitMeta().display); if (action.moisture.hasValue()) { diff --git a/examples/qt_tls_client/CMakeLists.txt b/examples/qt_tls_client/CMakeLists.txt index 766099997..ccde487e3 100644 --- a/examples/qt_tls_client/CMakeLists.txt +++ b/examples/qt_tls_client/CMakeLists.txt @@ -13,8 +13,9 @@ apply_warnings(morph_qt_tls_example) apply_bigobj(morph_qt_tls_example) # This example is a ctest case (`qt_tls_example_runs`, below), so on a -# sanitizer preset it is a suite the job runs -- and, until morph#542, one it -# ran uninstrumented. It also links morph_qt_impl, which is instrumented on +# sanitizer preset it is a suite the job runs, so it has to be instrumented or +# the job reports a clean run over a suite it could not see into. It also links +# morph_qt_impl, which is instrumented on # those presets; a consumer that is not would fail to link against it, the way # morph_test_main's comment in the root CMakeLists.txt records. if(DEFINED AF_SANITIZER) diff --git a/examples/rungs.txt b/examples/rungs.txt index 44ff5e187..e22b240af 100644 --- a/examples/rungs.txt +++ b/examples/rungs.txt @@ -38,7 +38,7 @@ # Not listed here, deliberately: examples/bank. Bank predates the ladder -- # examples/LADDER.md's intro cites it as prior art and its effort accounting # measures rungs in "bank-equivalents" -- and it carries no rung number; that -# is the ladder-slot question morph#87 raises, answered by leaving bank +# is the ladder-slot question, answered by leaving bank # outside the sequence, and examples/LADDER.md records the answer next to the # numbered table. Bank still ships a server (examples/bank/src/server/main.cpp, # built by a plain add_executable() in examples/bank/CMakeLists.txt) and a From 8ac26d35e63b6cd58cc0e7b139bfc04da74224ac Mon Sep 17 00:00:00 2001 From: Yaraslau Tamashevich Date: Thu, 24 Sep 2026 03:24:30 +0200 Subject: [PATCH 10/10] comments(examples): reflow the paragraphs the citation removals left ragged Whitespace only, and confined to comment paragraphs the earlier commits in this branch already touched: dropping a parenthetical citation mid-sentence leaves one short line, and this pulls the following words up so the paragraph reads at its usual width again. No wording changes, no code. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW --- examples/bank/gui/controllers/Format.hpp | 12 ++-- .../offline/lightweight_offline_queue.hpp | 10 ++-- examples/bank/tests/bank_test_support.hpp | 12 ++-- .../bank/tests/gui/test_bank_gui_format.cpp | 17 +++--- examples/common/testkit/test_db_fixture.cpp | 9 ++- .../crm/include/crm/offline/field_outbox.hpp | 8 +-- examples/crm/src/db/schema.cpp | 12 ++-- examples/crm/tests/test_dynamic_logic.cpp | 9 ++- examples/crm/tests/test_offline_sync.cpp | 6 +- examples/crm/tests/test_pipeline.cpp | 6 +- examples/kanban/gui/qml/RulesView.qml | 14 ++--- examples/kanban/gui_lib/kanban_schemas.hpp | 5 +- examples/kanban/include/kanban/core/types.hpp | 4 +- .../include/kanban/models/board_model.hpp | 4 +- examples/kanban/src/db/schema.cpp | 12 ++-- examples/kanban/src/models/board_model.cpp | 57 +++++++++---------- examples/kanban/tests/test_board_model.cpp | 28 +++++---- .../test_board_model_journal_outcome.cpp | 27 +++++---- .../tests/test_board_post_commit_tail.cpp | 4 +- .../kanban/tests/test_gui_forms_render.cpp | 6 +- examples/kanban/tests/test_kanban_offline.cpp | 4 +- .../kanban/tests/test_kanban_qml_surface.cpp | 4 +- .../tests/test_project_admin_qml_bridge.cpp | 10 ++-- examples/ledger/gui/main.cpp | 6 +- examples/ledger/gui/qml/LedgerView.qml | 6 +- examples/ledger/include/ledger/app/app.hpp | 13 ++--- .../ledger/include/ledger/core/time_util.hpp | 5 +- .../ledger/include/ledger/db/book_access.hpp | 15 +++-- .../include/ledger/db/ledger_entity.hpp | 10 ++-- examples/ledger/src/db/schema.cpp | 37 ++++++------ examples/ledger/src/models/budget_model.cpp | 14 ++--- examples/ledger/src/models/ledger_model.cpp | 56 +++++++++--------- .../tests/test_ledger_category_scope.cpp | 6 +- .../tests/test_ledger_list_transactions.cpp | 8 +-- examples/ledger/tests/test_ledger_model.cpp | 14 ++--- .../ledger/tests/test_ledger_model_keys.cpp | 4 +- examples/ledger/tests/test_ledger_reports.cpp | 6 +- .../ledger/tests/test_report_presenter.cpp | 10 ++-- .../lims/gui_lib/lims_qml_conversions.hpp | 6 +- .../lims/include/lims/core/self_journal.hpp | 6 +- .../lims/include/lims/dto/offline_dto.hpp | 4 +- examples/lims/src/db/schema.cpp | 12 ++-- .../src/models/analysis_catalog_model.cpp | 15 +++-- examples/lims/tests/test_backend_matrix.cpp | 4 +- examples/lims/tests/test_offline_capture.cpp | 8 +-- examples/lims/tests/test_result_entry.cpp | 12 ++-- 46 files changed, 271 insertions(+), 286 deletions(-) diff --git a/examples/bank/gui/controllers/Format.hpp b/examples/bank/gui/controllers/Format.hpp index ee78a27a4..47d5d3260 100644 --- a/examples/bank/gui/controllers/Format.hpp +++ b/examples/bank/gui/controllers/Format.hpp @@ -116,10 +116,10 @@ inline std::optional parseMinor(const QString& text, int decimals // because `nan < 0.0` is false -- is rejected rather than let through. // // The guard is still what makes the line below defined, and it still runs - // first. It bounds the *unrounded* value, which is the - // stronger of the two: every `double` strictly below 2^63 is at most - // 2^63-1024, so its rounding is inside `std::int64_t` with room to spare, - // and the bound stays the one form that is exact. + // first. It bounds the *unrounded* value, which is the stronger of the two: + // every `double` strictly below 2^63 is at most 2^63-1024, so its rounding + // is inside `std::int64_t` with room to spare, and the bound stays the one + // form that is exact. if (!(scaled < kMinorUnitsBound)) { return std::nullopt; } @@ -127,8 +127,8 @@ inline std::optional parseMinor(const QString& text, int decimals // on the double immediately below one half. 0.49999999999999994 + 0.5 is // exactly 1.0 in IEEE-754 -- the sum is not representable and rounds up -- // so truncating it charged a whole minor unit for an amount below half of - // one. `llround` rounds to nearest with halves away from zero, - // which is what the `+ 0.5` was reaching for. + // one. `llround` rounds to nearest with halves away from zero, which is + // what the `+ 0.5` was reaching for. return static_cast(std::llround(scaled)); } diff --git a/examples/bank/include/bank/offline/lightweight_offline_queue.hpp b/examples/bank/include/bank/offline/lightweight_offline_queue.hpp index e66dc1746..15240fb0b 100644 --- a/examples/bank/include/bank/offline/lightweight_offline_queue.hpp +++ b/examples/bank/include/bank/offline/lightweight_offline_queue.hpp @@ -19,11 +19,11 @@ /// morph owns the interface; the store is the application's. Nothing here is /// visible to `include/morph/` — `grep -rn Lightweight include/morph/` returns /// nothing, and that is the invariant this file exists to demonstrate rather -/// than to weaken. The framework's own durable queues -/// (`FileOfflineQueue`, `SqliteOfflineQueue`) are unchanged and unaffected: an -/// application that already has an ORM connection open does not need a second -/// persistence mechanism shipped by the framework, it needs the framework to -/// accept the one it has. +/// than to weaken. The framework's own durable queues (`FileOfflineQueue`, +/// `SqliteOfflineQueue`) are unchanged and unaffected: an application that +/// already has an ORM connection open does not need a second persistence +/// mechanism shipped by the framework, it needs the framework to accept the one +/// it has. namespace bank::offline { diff --git a/examples/bank/tests/bank_test_support.hpp b/examples/bank/tests/bank_test_support.hpp index 19c170704..8f1998007 100644 --- a/examples/bank/tests/bank_test_support.hpp +++ b/examples/bank/tests/bank_test_support.hpp @@ -113,12 +113,12 @@ T await(morph::async::Completion completion, morph::exec::MainThreadExecutor& // `static_assert` block below pins every route back to the hazard. // // The same two types, with the same names and the same explicit constructors, -// are what `tests/test_support.hpp`'s framework `waitUntil` takes, -// and what `examples/kanban/tests/test_kanban_stress.cpp` carries -- the same -// shape in three places rather than three shapes. They are redeclared here -// because bank deliberately links neither `morph_ladder_testkit` nor the -// framework's private test headers (see `examples/bank/CMakeLists.txt`'s own -// note on why bank is not a ladder rung). +// are what `tests/test_support.hpp`'s framework `waitUntil` takes, and what +// `examples/kanban/tests/test_kanban_stress.cpp` carries -- the same shape in +// three places rather than three shapes. They are redeclared here because bank +// deliberately links neither `morph_ladder_testkit` nor the framework's private +// test headers (see `examples/bank/CMakeLists.txt`'s own note on why bank is +// not a ladder rung). // // A `NOLINT` is not an option: it would remove the *warning* and leave the // hazard. Nor is widening one parameter's type to silence diff --git a/examples/bank/tests/gui/test_bank_gui_format.cpp b/examples/bank/tests/gui/test_bank_gui_format.cpp index 3a3d98777..c7fcde769 100644 --- a/examples/bank/tests/gui/test_bank_gui_format.cpp +++ b/examples/bank/tests/gui/test_bank_gui_format.cpp @@ -6,11 +6,11 @@ // // Why this file exists at all: `gui/controllers/Format.hpp` is a header under // `examples/`, and the root `.clang-tidy`'s `HeaderFilterRegex` discarded -// every finding in every such header, so no analyser had ever -// reported on it. What it contained was an unbounded `double` → `std::int64_t` -// conversion: `QString::toDouble` accepts `1e30`, `inf` and `nan` -// from a QML field that carries no validator, and converting any of those is -// undefined behaviour, not a large number. +// every finding in every such header, so no analyser had ever reported on it. +// What it contained was an unbounded `double` → `std::int64_t` conversion: +// `QString::toDouble` accepts `1e30`, `inf` and `nan` from a QML field that +// carries no validator, and converting any of those is undefined behaviour, not +// a large number. // // The cases below are written against the returned `std::optional`, not // against the arithmetic, and that is deliberate: on a UBSan build the @@ -55,10 +55,9 @@ TEST_CASE("parseMinor rejects text that is not a non-negative amount", "[bank][g CHECK_FALSE(parseMinor(QStringLiteral("-1.00")).has_value()); } -// The int64 bound. Without it each of these returns -// `-9223372036854775808` — via undefined behaviour, and via an abort under -// UBSan — and every call site then feeds that through `.value_or(0)` into a -// balance. +// The int64 bound. Without it each of these returns `-9223372036854775808` — +// via undefined behaviour, and via an abort under UBSan — and every call site +// then feeds that through `.value_or(0)` into a balance. TEST_CASE("parseMinor rejects amounts that do not fit in int64 minor units", "[bank][gui][format]") { // The easiest value to reach it with: 1e30 major units scale to 1e32. CHECK_FALSE(parseMinor(QStringLiteral("1e30")).has_value()); diff --git a/examples/common/testkit/test_db_fixture.cpp b/examples/common/testkit/test_db_fixture.cpp index 13fae12bd..09bdd95d1 100644 --- a/examples/common/testkit/test_db_fixture.cpp +++ b/examples/common/testkit/test_db_fixture.cpp @@ -116,11 +116,10 @@ TEST_CASE("DbFixture::computeConnectionString uses ODBC_CONNECTION_STRING verbat } // When the shared database holds a foreign key whose target table is gone, -// Lightweight's SqlSchema::ReadAllTables throws -// std::out_of_range("map::at") out of DbFixture's constructor — before -// anything has been dropped, so the bad state survives the run that reported -// it and every test after it fails the same way, across invocations, until -// somebody deletes a file nothing names. +// Lightweight's SqlSchema::ReadAllTables throws std::out_of_range("map::at") +// out of DbFixture's constructor — before anything has been dropped, so the bad +// state survives the run that reported it and every test after it fails the +// same way, across invocations, until somebody deletes a file nothing names. // // Two test cases below, because the requirement has two halves and either can // hold without the other: the message has to name the fixture, the file and diff --git a/examples/crm/include/crm/offline/field_outbox.hpp b/examples/crm/include/crm/offline/field_outbox.hpp index 4c440585a..03823cdb8 100644 --- a/examples/crm/include/crm/offline/field_outbox.hpp +++ b/examples/crm/include/crm/offline/field_outbox.hpp @@ -15,10 +15,10 @@ /// A field rep's **write path** for offline opportunity edits (README build /// order §8). Same shape, and same reasoning, as `lims::offline::FieldOutbox` /// (that rung's §7) — see that header's own doc comment for the full -/// argument (`examples/IMPLEMENTATION.md` rule 1's carve-out): -/// the framework supplies no seam for "detect an offline `execute()` and -/// queue instead", so the app decides that at the dispatch site, and this -/// class is that decision's domain-shaped half for crm's own entity. +/// argument (`examples/IMPLEMENTATION.md` rule 1's carve-out): the framework +/// supplies no seam for "detect an offline `execute()` and queue instead", so +/// the app decides that at the dispatch site, and this class is that decision's +/// domain-shaped half for crm's own entity. /// /// @par What it actually does: chain a rep's own edits /// The same ODK Central trap lims's rung names: a rep who edits the same diff --git a/examples/crm/src/db/schema.cpp b/examples/crm/src/db/schema.cpp index d4696f688..676bcab87 100644 --- a/examples/crm/src/db/schema.cpp +++ b/examples/crm/src/db/schema.cpp @@ -8,12 +8,12 @@ namespace crm::db { void configure(const std::string& connectionString) { - // Nothing in Lightweight stops a pooled DataMapper from being - // returned with a transaction still open on it -- `DataMapperPool::Return` - // does no transaction cleanup, and the cost lands on the next, unrelated - // borrower as a 60s stall and a `database is locked` it did not cause. - // Installing the audit here, at the one point every rung's process - // configures its database, turns that into an abort at the leak. See + // Nothing in Lightweight stops a pooled DataMapper from being returned with + // a transaction still open on it -- `DataMapperPool::Return` does no + // transaction cleanup, and the cost lands on the next, unrelated borrower + // as a 60s stall and a `database is locked` it did not cause. Installing + // the audit here, at the one point every rung's process configures its + // database, turns that into an abort at the leak. See // examples/common/db/pool_transaction_audit.hpp. (void)::morph::ladder::db::installPoolTransactionAudit(); diff --git a/examples/crm/tests/test_dynamic_logic.cpp b/examples/crm/tests/test_dynamic_logic.cpp index 512da5236..e5ca151a4 100644 --- a/examples/crm/tests/test_dynamic_logic.cpp +++ b/examples/crm/tests/test_dynamic_logic.cpp @@ -2,11 +2,10 @@ // // Dynamic logic (README build order §7): CreateOpportunity/UpdateOpportunity // declare requiredWhen(primaryContact, engaged(expectedCloseValue)) — the -// shipped And/Or/Not-capable rule combinator vocabulary, used -// here as a plain requiredWhen (no compound and/or/not needed for this -// particular rule, since the fields available don't support the -// stage-comparison this rung originally considered — see -// opportunity_dto.hpp's doc comment for why). +// shipped And/Or/Not-capable rule combinator vocabulary, used here as a plain +// requiredWhen (no compound and/or/not needed for this particular rule, since +// the fields available don't support the stage-comparison this rung originally +// considered — see opportunity_dto.hpp's doc comment for why). #include #include diff --git a/examples/crm/tests/test_offline_sync.cpp b/examples/crm/tests/test_offline_sync.cpp index c3c8194a5..9fb7a1580 100644 --- a/examples/crm/tests/test_offline_sync.cpp +++ b/examples/crm/tests/test_offline_sync.cpp @@ -16,9 +16,9 @@ // ── GCC 16's -Warray-bounds false positive inside libstdc++'s shared_ptr ──── // -// The same block as `test_pipeline.cpp`'s -- which carries the -// full measurement, the sizes, and the reason this wraps only the `#include` -// block. Short form: at -O2/-O3, GCC 16 speculatively devirtualizes +// The same block as `test_pipeline.cpp`'s -- which carries the full +// measurement, the sizes, and the reason this wraps only the `#include` block. +// Short form: at -O2/-O3, GCC 16 speculatively devirtualizes // `ModelFactory::create()`'s `IModelHolder::attachActionLog` // to `ModelHolder`'s override, then bounds an offset-112 // store against the 112-byte `AccountModel` holder instead of the 136-byte diff --git a/examples/crm/tests/test_pipeline.cpp b/examples/crm/tests/test_pipeline.cpp index 98f6ecea0..7a4fe9072 100644 --- a/examples/crm/tests/test_pipeline.cpp +++ b/examples/crm/tests/test_pipeline.cpp @@ -7,9 +7,9 @@ // ── GCC 16's -Warray-bounds false positive inside libstdc++'s shared_ptr ──── // -// On `g++ (GCC) 16.2.1` at -O2/-O3 with -Werror, this translation -// unit and `test_offline_sync.cpp` are the only two in the tree that fail to -// compile, on a diagnostic raised entirely inside libstdc++: +// On `g++ (GCC) 16.2.1` at -O2/-O3 with -Werror, this translation unit and +// `test_offline_sync.cpp` are the only two in the tree that fail to compile, on +// a diagnostic raised entirely inside libstdc++: // // /usr/include/c++/16/bits/shared_ptr_base.h:1165:32: error: array // subscript 14 is outside array bounds of 'void [112]' diff --git a/examples/kanban/gui/qml/RulesView.qml b/examples/kanban/gui/qml/RulesView.qml index b9f01e834..8aaa15e91 100644 --- a/examples/kanban/gui/qml/RulesView.qml +++ b/examples/kanban/gui/qml/RulesView.qml @@ -10,13 +10,13 @@ // affordance, since RuleTriggerEvent has exactly one member (rule_dto.hpp). // // Both of this rung's remaining hand-built forms are schema-driven: -// `CreateRule::mutationType` (a `RuleMutationType` enum class) -// renders as the combo box DynamicForm draws for a closed -// `oneOf`-of-`const`s set, and `CreateRule::triggerColumnId` is a -// `ColumnId` to a `morph::forms::Choice<…, "GetBoardState">` (rule 3's shape -// for a user-chosen foreign key) -- so the trigger-column combo box is now -// server-fetched by DynamicForm itself, via BoardBridge.fetchOptions(), rather -// than reading BoardBridge.board.columns by hand. +// `CreateRule::mutationType` (a `RuleMutationType` enum class) renders as the +// combo box DynamicForm draws for a closed `oneOf`-of-`const`s set, and +// `CreateRule::triggerColumnId` is a `ColumnId` to a `morph::forms::Choice<…, +// "GetBoardState">` (rule 3's shape for a user-chosen foreign key) -- so the +// trigger-column combo box is now server-fetched by DynamicForm itself, via +// BoardBridge.fetchOptions(), rather than reading BoardBridge.board.columns by +// hand. // // `boardBridge` defaults to null so this same file also loads standalone // with nothing wired up, matching MembersView.qml's identical convention. diff --git a/examples/kanban/gui_lib/kanban_schemas.hpp b/examples/kanban/gui_lib/kanban_schemas.hpp index feac6eff5..df5f69c89 100644 --- a/examples/kanban/gui_lib/kanban_schemas.hpp +++ b/examples/kanban/gui_lib/kanban_schemas.hpp @@ -38,9 +38,8 @@ /// `CreateRule::triggerColumnId` is a `morph::forms::Choice<…, /// "GetBoardState">` rather than a raw `ColumnId` — the shape rule 3 prescribes /// for a user-chosen foreign key; `GetBoardState`'s reply returns `columns` as -/// its first array member. -/// `MoveTaskPosition` is absent for a different reason: it is a drag -/// gesture, not a form (rule 2(a); see the README). +/// its first array member. `MoveTaskPosition` is absent for a different reason: +/// it is a drag gesture, not a form (rule 2(a); see the README). namespace kanban::gui { diff --git a/examples/kanban/include/kanban/core/types.hpp b/examples/kanban/include/kanban/core/types.hpp index 2d2aaa9e8..1050563d1 100644 --- a/examples/kanban/include/kanban/core/types.hpp +++ b/examples/kanban/include/kanban/core/types.hpp @@ -105,8 +105,8 @@ enum class Role : std::uint8_t { Viewer, Member, Manager }; /// `value == 0` *is* the "not entered" state, **an event id of `0` is /// unrepresentable** -- construct one and it reports `hasValue() == false`, /// so a real event would read as "no event". The constraint holds because these -/// ids are SQLite row ids, which start at 1. `fromRowId()` is -/// the enforcement; use it for every conversion from a stored value. +/// ids are SQLite row ids, which start at 1. `fromRowId()` is the enforcement; +/// use it for every conversion from a stored value. struct BoardEventId { std::int64_t value{0}; [[nodiscard]] constexpr bool hasValue() const { return value != 0; } diff --git a/examples/kanban/include/kanban/models/board_model.hpp b/examples/kanban/include/kanban/models/board_model.hpp index 888ef719a..36ae53418 100644 --- a/examples/kanban/include/kanban/models/board_model.hpp +++ b/examples/kanban/include/kanban/models/board_model.hpp @@ -570,8 +570,8 @@ BRIDGE_REGISTER_ACTION(kanban::BoardModel, kanban::RemoveAttachment, "RemoveAtta // scalars. // // The disengaged-`projectId` rejection a hand-written `key()` would spell out is -// `morph::model::keyToString`'s: it throws for a strong id with no -// value instead of dereferencing an empty optional, which is what makes +// `morph::model::keyToString`'s: it throws for a strong id with no value +// instead of dereferencing an empty optional, which is what makes // `BoardBridge::openBoard("not-a-number")` (parsed into a default-constructed // `ProjectId{}` by board_qml_bridge.cpp's `parseId`) a rejected `Completion` // rather than undefined behaviour. `BridgeHandler::execute`'s diff --git a/examples/kanban/src/db/schema.cpp b/examples/kanban/src/db/schema.cpp index 40f05c235..527ea6e55 100644 --- a/examples/kanban/src/db/schema.cpp +++ b/examples/kanban/src/db/schema.cpp @@ -9,12 +9,12 @@ namespace kanban::db { void setup(const std::string& connectionString) { - // Nothing in Lightweight stops a pooled DataMapper from being - // returned with a transaction still open on it -- `DataMapperPool::Return` - // does no transaction cleanup, and the cost lands on the next, unrelated - // borrower as a 60s stall and a `database is locked` it did not cause. - // Installing the audit here, at the one point every rung's process - // configures its database, turns that into an abort at the leak. See + // Nothing in Lightweight stops a pooled DataMapper from being returned with + // a transaction still open on it -- `DataMapperPool::Return` does no + // transaction cleanup, and the cost lands on the next, unrelated borrower + // as a 60s stall and a `database is locked` it did not cause. Installing + // the audit here, at the one point every rung's process configures its + // database, turns that into an abort at the leak. See // examples/common/db/pool_transaction_audit.hpp. (void)::morph::ladder::db::installPoolTransactionAudit(); diff --git a/examples/kanban/src/models/board_model.cpp b/examples/kanban/src/models/board_model.cpp index 03c470e0f..18ec8c61c 100644 --- a/examples/kanban/src/models/board_model.cpp +++ b/examples/kanban/src/models/board_model.cpp @@ -217,15 +217,14 @@ template /// its three siblings therefore build their `GetBoardResult` inside the /// transaction, which is where `MoveTaskPosition` builds its own (it needs one /// for the applied-ops ledger row). That is not a workaround for this -/// overload's lack -/// of a fallback -- it is the stronger ordering. A re-read that fails *before* -/// the commit rolls the write back, so the caller's "this failed" is true; -/// a re-read that fails *after* it leaves nothing truthful to return, because -/// the board state is the answer and there is no partial board worth sending. -/// The cost is that the write transaction now spans the read, so it holds -/// SQLite's write lock for longer under contention; `MoveTaskPosition`, the -/// heaviest handler in this file, has held it across exactly that read since -/// this rung was written. +/// overload's lack of a fallback -- it is the stronger ordering. A re-read that +/// fails *before* the commit rolls the write back, so the caller's "this +/// failed" is true; a re-read that fails *after* it leaves nothing truthful to +/// return, because the board state is the answer and there is no partial board +/// worth sending. The cost is that the write transaction now spans the read, so +/// it holds SQLite's write lock for longer under contention; +/// `MoveTaskPosition`, the heaviest handler in this file, has held it across +/// exactly that read since this rung was written. /// /// @tparam Tail Nullary callable returning `void`. /// @param tail The post-commit work to run. @@ -584,11 +583,11 @@ GetBoardResult BoardModel::execute(const CreateColumn& action) { event.createdAtMs = nowMs(); mapper->Create(event); - // Built before the commit, not after it. The board state - // is this call's whole return value, so a re-read that fails must roll - // the write back rather than leave a committed mutation with nothing - // truthful to report -- see `runPostCommitTail`'s void overload for the - // reasoning and its cost. `MoveTaskPosition` has always read here. + // Built before the commit, not after it. The board state is this call's + // whole return value, so a re-read that fails must roll the write back + // rather than leave a committed mutation with nothing truthful to + // report -- see `runPostCommitTail`'s void overload for the reasoning + // and its cost. `MoveTaskPosition` has always read here. auto result = buildState(mapper.Get(), project); transaction.Commit(); @@ -636,11 +635,11 @@ GetBoardResult BoardModel::execute(const CreateSwimlane& action) { event.createdAtMs = nowMs(); mapper->Create(event); - // Built before the commit, not after it. The board state - // is this call's whole return value, so a re-read that fails must roll - // the write back rather than leave a committed mutation with nothing - // truthful to report -- see `runPostCommitTail`'s void overload for the - // reasoning and its cost. `MoveTaskPosition` has always read here. + // Built before the commit, not after it. The board state is this call's + // whole return value, so a re-read that fails must roll the write back + // rather than leave a committed mutation with nothing truthful to + // report -- see `runPostCommitTail`'s void overload for the reasoning + // and its cost. `MoveTaskPosition` has always read here. auto result = buildState(mapper.Get(), project); transaction.Commit(); @@ -703,11 +702,11 @@ GetBoardResult BoardModel::execute(const CreateTask& action) { event.createdAtMs = nowMs(); mapper->Create(event); - // Built before the commit, not after it. The board state - // is this call's whole return value, so a re-read that fails must roll - // the write back rather than leave a committed mutation with nothing - // truthful to report -- see `runPostCommitTail`'s void overload for the - // reasoning and its cost. `MoveTaskPosition` has always read here. + // Built before the commit, not after it. The board state is this call's + // whole return value, so a re-read that fails must roll the write back + // rather than leave a committed mutation with nothing truthful to + // report -- see `runPostCommitTail`'s void overload for the reasoning + // and its cost. `MoveTaskPosition` has always read here. auto result = buildState(mapper.Get(), project); transaction.Commit(); @@ -758,11 +757,11 @@ GetBoardResult BoardModel::execute(const AddComment& action) { event.createdAtMs = nowMs(); mapper->Create(event); - // Built before the commit, not after it. The board state - // is this call's whole return value, so a re-read that fails must roll - // the write back rather than leave a committed mutation with nothing - // truthful to report -- see `runPostCommitTail`'s void overload for the - // reasoning and its cost. `MoveTaskPosition` has always read here. + // Built before the commit, not after it. The board state is this call's + // whole return value, so a re-read that fails must roll the write back + // rather than leave a committed mutation with nothing truthful to + // report -- see `runPostCommitTail`'s void overload for the reasoning + // and its cost. `MoveTaskPosition` has always read here. auto result = buildState(mapper.Get(), project); transaction.Commit(); diff --git a/examples/kanban/tests/test_board_model.cpp b/examples/kanban/tests/test_board_model.cpp index da5b02b88..50e499623 100644 --- a/examples/kanban/tests/test_board_model.cpp +++ b/examples/kanban/tests/test_board_model.cpp @@ -832,10 +832,9 @@ TEST_CASE("Replaying a move-to-Done journal entry does not re-fire its rule", "[ // project B's rules would answer with A's and an `ok`: the argument would be // load-bearing in the type and inert in the code, and a client that passed the // id it meant would have no way to tell it had been ignored. Both refuse a -// mismatch the way every other cross-project check in -// board_model.cpp does. alice is a Manager on *both* projects here, so what -// these pin is the projectId check itself, not the role gate standing in -// for it. +// mismatch the way every other cross-project check in board_model.cpp does. +// alice is a Manager on *both* projects here, so what these pin is the +// projectId check itself, not the role gate standing in for it. TEST_CASE("GetRules refuses another project's id rather than answering with the attached board's rules", "[kanban][rules][cross-tenant]") { @@ -1042,12 +1041,12 @@ TEST_CASE("ActionKeyTraits::key() rejects a disengaged projectId inst CHECK_THROWS_AS(morph::model::ActionKeyTraits::key(disengaged), std::runtime_error); // The refusal is now `morph::model::keyToString`'s own, not this rung's: - // BRIDGE_MODEL_KEY generates the specialisation, so the - // hand-written `throw kanban::ValidationError` is gone and the escaping - // type is morph's plain `std::runtime_error`. Pinned as a negative - // because it is the one caller-visible difference the migration makes. - // Nothing in the rung catches this by type -- `BoardBridge` routes it - // through `morph::ladder::gui::errorText()`, which reads `what()` off any + // BRIDGE_MODEL_KEY generates the specialisation, so the hand-written `throw + // kanban::ValidationError` is gone and the escaping type is morph's plain + // `std::runtime_error`. Pinned as a negative because it is the one + // caller-visible difference the migration makes. Nothing in the rung + // catches this by type -- `BoardBridge` routes it through + // `morph::ladder::gui::errorText()`, which reads `what()` off any // `std::exception` -- but a change that quietly reintroduced a rung-local // `key()` would show up right here. bool refusedByTheRung = false; @@ -1061,11 +1060,10 @@ TEST_CASE("ActionKeyTraits::key() rejects a disengaged projectId inst CHECK_FALSE(refusedByTheRung); } -// Equivalence pin: one -// `BRIDGE_MODEL_KEY(BoardModel, OpenBoard, &OpenBoard::projectId)` stands in for -// two hand-written specialisations, and the only way that is safe is if the -// generated key is the same bytes a hand-written `key()` produces, for every id -// it accepts. +// Equivalence pin: one `BRIDGE_MODEL_KEY(BoardModel, OpenBoard, +// &OpenBoard::projectId)` stands in for two hand-written specialisations, and +// the only way that is safe is if the generated key is the same bytes a +// hand-written `key()` produces, for every id it accepts. TEST_CASE("BoardModel's deduced key is the ProjectId itself and encodes exactly as the hand-written key() did", "[kanban][model][key]") { // The macro deduces `PrimaryKey` from the *member* it is handed, so the diff --git a/examples/kanban/tests/test_board_model_journal_outcome.cpp b/examples/kanban/tests/test_board_model_journal_outcome.cpp index 1a64b17f2..1866888a9 100644 --- a/examples/kanban/tests/test_board_model_journal_outcome.cpp +++ b/examples/kanban/tests/test_board_model_journal_outcome.cpp @@ -22,12 +22,11 @@ // rung's own refusals and for nothing else -- an `std::invalid_argument` out of // `std::stoull`, one of Lightweight's SQL exceptions from a pre-commit query, or // a `std::bad_alloc` would all reach the caller having journalled nothing at -// all. Nothing in -// "a rejected attempt is itself audit-worthy" depends on the exception's -// type; only the `catch` clause did. The last three cases in this file cover -// the widened path, including the failure mode widening it introduces: the -// journal write is itself fallible, and on a failure path a throw from it -// must not replace the exception the caller came for. +// all. Nothing in "a rejected attempt is itself audit-worthy" depends on the +// exception's type; only the `catch` clause did. The last three cases in this +// file cover the widened path, including the failure mode widening it +// introduces: the journal write is itself fallible, and on a failure path a +// throw from it must not replace the exception the caller came for. #include #include @@ -215,10 +214,10 @@ struct FailedEntry { /// `do { try { ... } catch (...) { ... } } while (... && ...)`, which /// `readability-function-cognitive-complexity` scores at 4. Six of them /// written out in the `TEST_CASE` below put it at 32 against a threshold of -/// 25, which fails `clang-tidy-diff`. The assertions are the same -/// assertions and there are the same number of them -- they are reported -/// against this function's lines instead of the caller's, which is the whole -/// of the difference. +/// 25, which fails `clang-tidy-diff`. The assertions are the same assertions +/// and there are the same number of them -- they are reported against this +/// function's lines instead of the caller's, which is the whole of the +/// difference. /// /// `error` is deliberately not also checked for emptiness: @p errorPrefix is /// non-empty at every call site, and a string that starts with a non-empty @@ -373,10 +372,10 @@ TEST_CASE("A journal that throws on the failure path does not replace a non-doma const DbBusyFixture busy{"board_columns"}; // The compounded case: a non-domain failure -- the class a narrower catch - // would not journal at all -- while the journal it writes - // to is itself failing. `ValidationError` above proves containment by the - // exception's type; here there is no type to lean on, so the message is - // what distinguishes the original failure from the logging one. + // would not journal at all -- while the journal it writes to is itself + // failing. `ValidationError` above proves containment by the exception's + // type; here there is no type to lean on, so the message is what + // distinguishes the original failure from the logging one. const auto reached = messageFrom([&] { (void)model.execute(kanban::CreateColumn{.name = "To Do", .wipLimit = 0}); }); drained.clear(); diff --git a/examples/kanban/tests/test_board_post_commit_tail.cpp b/examples/kanban/tests/test_board_post_commit_tail.cpp index 16b3b93a3..dd31e9f5a 100644 --- a/examples/kanban/tests/test_board_post_commit_tail.cpp +++ b/examples/kanban/tests/test_board_post_commit_tail.cpp @@ -4,8 +4,8 @@ // as failed -- the row it wrote is already committed. // // The symptom is a board whose `execute(MoveTaskPosition)` threw and whose move -// was nonetheless observed applied (`test_kanban_offline.cpp:672`, -// `movedCount == 1` under 32-way contention). That needs no race inside the +// was nonetheless observed applied (`test_kanban_offline.cpp:672`, `movedCount +// == 1` under 32-way contention). That needs no race inside the // commit path: `execute()` commits, then runs `logAction`, `evaluateRules` and a // final `buildState` -- every one of which can throw -- and without the shield // nothing between them and the caller distinguishes "the move did not happen" diff --git a/examples/kanban/tests/test_gui_forms_render.cpp b/examples/kanban/tests/test_gui_forms_render.cpp index 80779942e..c6661cfb6 100644 --- a/examples/kanban/tests/test_gui_forms_render.cpp +++ b/examples/kanban/tests/test_gui_forms_render.cpp @@ -327,9 +327,9 @@ TEST_CASE("MembersView renders SetMemberRole through the shipped renderer and su // currentIndex -1 -- "no selection" -- so the gate needs `role` engaged // too, not just `principal` (DynamicForm.qml's resetFields()/currentIndex // comments). Membership is decidable client-side once the schema states - // the closed set: an out-of-set value here would leave the - // field's own JSON literal null and the gate unsatisfied, which is a - // stronger property than the free-text field this form replaced ever had. + // the closed set: an out-of-set value here would leave the field's own JSON + // literal null and the gate unsatisfied, which is a stronger property than + // the free-text field this form replaced ever had. CHECK_FALSE(isReady(form)); type(form, QStringLiteral("principal"), QStringLiteral("bob")); CHECK_FALSE(isReady(form)); diff --git a/examples/kanban/tests/test_kanban_offline.cpp b/examples/kanban/tests/test_kanban_offline.cpp index 95cb39d9b..c7947e6b8 100644 --- a/examples/kanban/tests/test_kanban_offline.cpp +++ b/examples/kanban/tests/test_kanban_offline.cpp @@ -158,8 +158,8 @@ TEST_CASE("Dropping MoveTaskPosition's reply frame and retrying is exactly-once, // be destroyed *before* the Bridge, and the handler would write into dead // stack slots: AddressSanitizer reports precisely that as a // stack-use-after-scope (caught by the ladder's ASan+UBSan leg). Same - // hazard, and same cause, as any callback outliving the frame - // it captured by reference. + // hazard, and same cause, as any callback outliving the frame it captured + // by reference. bool firstResolved = false; bool firstFailed = false; diff --git a/examples/kanban/tests/test_kanban_qml_surface.cpp b/examples/kanban/tests/test_kanban_qml_surface.cpp index 07d48a987..91d779004 100644 --- a/examples/kanban/tests/test_kanban_qml_surface.cpp +++ b/examples/kanban/tests/test_kanban_qml_surface.cpp @@ -133,8 +133,8 @@ TEST_CASE("Every kanban bridge exposes exactly the surface gui/qml binds, and no audit.allowUnbound(QStringLiteral("projectAdminBridge"), QStringLiteral("submitIfValid"), rendererCalled); audit.allowUnbound(QStringLiteral("boardBridge"), QStringLiteral("submitIfValid"), rendererCalled); // `fetchOptions`/`optionsReceived` are the same seam, one field kind over: - // `CreateRule::triggerColumnId` is this rung's first - // `morph::forms::Choice` field, and DynamicForm.qml's own + // `CreateRule::triggerColumnId` is this rung's first `morph::forms::Choice` + // field, and DynamicForm.qml's own // Component.onCompleted calls `controller.fetchOptions(...)` and its // `Connections { target: controller }` block declares `onOptionsReceived` // unconditionally for every attached controller (see diff --git a/examples/kanban/tests/test_project_admin_qml_bridge.cpp b/examples/kanban/tests/test_project_admin_qml_bridge.cpp index f02d2f7a8..4050c20af 100644 --- a/examples/kanban/tests/test_project_admin_qml_bridge.cpp +++ b/examples/kanban/tests/test_project_admin_qml_bridge.cpp @@ -406,11 +406,11 @@ TEST_CASE("ProjectAdminBridge::submitIfValid creates a project and still emits p TEST_CASE("ProjectAdminBridge::submitIfValid sets a member role and still emits memberRoleSet", "[kanban][gui][qml-bridge][issue344][issue393]") { - // MembersView.qml's "add member" row is a DynamicForm, so - // submitForm's SetMemberRole branch decodes a bare Ack and re-emits - // memberRoleSet exactly as the typed setMemberRole() call does, so - // whichever path submitted, ProjectListView.qml's existing - // onMemberRoleSet -> listRoles() handler still fires. + // MembersView.qml's "add member" row is a DynamicForm, so submitForm's + // SetMemberRole branch decodes a bare Ack and re-emits memberRoleSet + // exactly as the typed setMemberRole() call does, so whichever path + // submitted, ProjectListView.qml's existing onMemberRoleSet -> listRoles() + // handler still fires. DbFixture fixture; auto rig = makeAuthedRig("alice"); kanban::gui::ProjectAdminBridge bridge{rig->bridge(0), rig->executor()}; diff --git a/examples/ledger/gui/main.cpp b/examples/ledger/gui/main.cpp index 7dac7ae7a..cd834ae0e 100644 --- a/examples/ledger/gui/main.cpp +++ b/examples/ledger/gui/main.cpp @@ -2,9 +2,9 @@ // /// @file /// ledger's desktop client shell: one `AppContext` (deployment mode chosen -/// from argv), a login step before any domain bridge is usable, -/// the four QML bridges Tasks 18-21 define built inside `ctx.onReady()`, and -/// a `QQmlApplicationEngine` loading this rung's own QML module. +/// from argv), a login step before any domain bridge is usable, the four QML +/// bridges Tasks 18-21 define built inside `ctx.onReady()`, and a +/// `QQmlApplicationEngine` loading this rung's own QML module. /// /// Bridges are passed as *initial properties*, not context properties, so the /// same `Main.qml` also loads with nothing wired -- which is exactly what the diff --git a/examples/ledger/gui/qml/LedgerView.qml b/examples/ledger/gui/qml/LedgerView.qml index 2713edfcc..d15655f8d 100644 --- a/examples/ledger/gui/qml/LedgerView.qml +++ b/examples/ledger/gui/qml/LedgerView.qml @@ -96,9 +96,9 @@ ColumnLayout { // ── Entries, and the Undo control they feed ────────────────────────── // The journal id is shown rather than typed. A bare "Journal id to undo" // TextField would be unfillable except by guessing: nothing else in this - // rung, and no reply on the wire, displays a journal id. - // `listTransactions` is where the ids come from; `undoTransaction` is - // handed one of them straight back. + // rung, and no reply on the wire, displays a journal id. `listTransactions` + // is where the ids come from; `undoTransaction` is handed one of them + // straight back. RowLayout { Layout.fillWidth: true Label { text: qsTr("Entries") ; font.bold: true } diff --git a/examples/ledger/include/ledger/app/app.hpp b/examples/ledger/include/ledger/app/app.hpp index dcf6c9ae7..0cfae59a1 100644 --- a/examples/ledger/include/ledger/app/app.hpp +++ b/examples/ledger/include/ledger/app/app.hpp @@ -16,8 +16,8 @@ /// @file /// `ledger::app::App` -- rung 5's server-side bootstrap: the model worker -/// pool, the `RemoteServer` a standalone `ladder_ledger_server` -/// stands a transport in front of, the process-global `TokenIssuer` and +/// pool, the `RemoteServer` a standalone `ladder_ledger_server` stands a +/// transport in front of, the process-global `TokenIssuer` and /// `LedgerAuthorizer` that give this rung a real auth story, and the one /// background job this rung has. /// @@ -25,11 +25,10 @@ /// background job puts it in a model instead: `LedgerModel` would own a /// `ThreadPoolExecutor`, `SubmitReport` would post the aggregation to it, and /// this would be the one ladder model including ``. -/// The layering that avoids that is the same one -/// `bookmarks::app::App` already demonstrates: the App owns the worker pool -/// and decides *when* work runs; the model still owns *what* the work -/// computes, and is re-entered as an ordinary client dispatch -/// (`RunReportJob`), on its own strand, where mutation is safe. +/// The layering that avoids that is the same one `bookmarks::app::App` already +/// demonstrates: the App owns the worker pool and decides *when* work runs; the +/// model still owns *what* the work computes, and is re-entered as an ordinary +/// client dispatch (`RunReportJob`), on its own strand, where mutation is safe. /// /// Mirrors `bookmarks::app::App` closely and on purpose -- the same /// `RemoteServer` + process-global `TokenIssuer` + service-token-carrying diff --git a/examples/ledger/include/ledger/core/time_util.hpp b/examples/ledger/include/ledger/core/time_util.hpp index 2fec5c82e..5505a2214 100644 --- a/examples/ledger/include/ledger/core/time_util.hpp +++ b/examples/ledger/include/ledger/core/time_util.hpp @@ -22,9 +22,8 @@ /// because they now have more than one. `isValidYearMonth` was in /// `ledger/dto/budget_dto.hpp` and `monthRangeMs` in an anonymous namespace in /// `src/models/budget_model.cpp`; `ListTransactions` takes the same -/// `"YYYY-MM"` bound as `GetBudgetReport` and must parse it the -/// same way, and a second copy of a date parser is a second thing to get -/// wrong. +/// `"YYYY-MM"` bound as `GetBudgetReport` and must parse it the same way, and a +/// second copy of a date parser is a second thing to get wrong. namespace ledger { diff --git a/examples/ledger/include/ledger/db/book_access.hpp b/examples/ledger/include/ledger/db/book_access.hpp index 3bfa696c7..0daa6b680 100644 --- a/examples/ledger/include/ledger/db/book_access.hpp +++ b/examples/ledger/include/ledger/db/book_access.hpp @@ -12,9 +12,8 @@ #include "ledger/db/ledger_entity.hpp" /// @file -/// The single home for this rung's "whose book is this?" rule, and -/// for the "*which* book is this?" rule that sits beside it -/// (`requireCategoryInBook`). +/// The single home for this rung's "whose book is this?" rule, and for the +/// "*which* book is this?" rule that sits beside it (`requireCategoryInBook`). /// /// **Where the rule lives, and why not at the authorizer.** /// `examples/IMPLEMENTATION.md` rule 4 puts ownership authorization *through @@ -158,11 +157,11 @@ inline void requireOwnedParentBook(Lightweight::DataMapper& mapper, std::uint64_ /// wrote down is half a scheme, not a rule. /// /// Neither `SetCategory` nor `LinkAccountToCategory` carries a `ledgerId`, so -/// this cannot be a `Where` folded into the lookup the way -/// `accountInLedger` scopes a leg's account against the ledger its action -/// names. It is a comparison of the two loaded rows' own `ledger` values -/// instead -- which is also why the refusal is raised after the not-found and -/// ownership ones, leaving their wording and ordering untouched. +/// this cannot be a `Where` folded into the lookup the way `accountInLedger` +/// scopes a leg's account against the ledger its action names. It is a +/// comparison of the two loaded rows' own `ledger` values instead -- which is +/// also why the refusal is raised after the not-found and ownership ones, +/// leaving their wording and ordering untouched. /// /// `NotFound`, and a message of its own: `accountInLedger`'s exact idiom for /// the identical question about an account, and for the same reason -- diff --git a/examples/ledger/include/ledger/db/ledger_entity.hpp b/examples/ledger/include/ledger/db/ledger_entity.hpp index b6a75b651..db89a5ef0 100644 --- a/examples/ledger/include/ledger/db/ledger_entity.hpp +++ b/examples/ledger/include/ledger/db/ledger_entity.hpp @@ -24,8 +24,8 @@ struct LedgerRecord { static constexpr std::string_view TableName = "ledgers"; Light::Field id; // 0 Light::Field, Light::SqlRealName{"name"}> name; // 1 - // The principal that created this book. Every action reaching - // this book compares it against `session::current()->principal`; see + // The principal that created this book. Every action reaching this book + // compares it against `session::current()->principal`; see // `ledger/db/book_access.hpp`, which is the single home for that rule. // // Nullable, and the nullability is load-bearing rather than incidental: @@ -197,9 +197,9 @@ struct ReportJobRecord { // `SubmitReport::params` verbatim, so the job row records what it was // asked to compute and not merely that it was asked. Load-bearing since // the aggregation moved out of SubmitReport's own call frame and into - // RunReportJob: the runner that eventually settles this job - // may be in a different process from the one that accepted it, and has - // nothing but this row to reconstruct the request from. + // RunReportJob: the runner that eventually settles this job may be in a + // different process from the one that accepted it, and has nothing but this + // row to reconstruct the request from. // // Nullable, unlike the other required columns, purely because it was // added by ALTER TABLE to a table that may already hold rows (schema.cpp's diff --git a/examples/ledger/src/db/schema.cpp b/examples/ledger/src/db/schema.cpp index 11e722f82..0763ce979 100644 --- a/examples/ledger/src/db/schema.cpp +++ b/examples/ledger/src/db/schema.cpp @@ -8,12 +8,12 @@ namespace ledger::db { void configure(const std::string& connectionString) { - // Nothing in Lightweight stops a pooled DataMapper from being - // returned with a transaction still open on it -- `DataMapperPool::Return` - // does no transaction cleanup, and the cost lands on the next, unrelated - // borrower as a 60s stall and a `database is locked` it did not cause. - // Installing the audit here, at the one point every rung's process - // configures its database, turns that into an abort at the leak. See + // Nothing in Lightweight stops a pooled DataMapper from being returned with + // a transaction still open on it -- `DataMapperPool::Return` does no + // transaction cleanup, and the cost lands on the next, unrelated borrower + // as a 60s stall and a `database is locked` it did not cause. Installing + // the audit here, at the one point every rung's process configures its + // database, turns that into an abort at the leak. See // examples/common/db/pool_transaction_audit.hpp. (void)::morph::ladder::db::installPoolTransactionAudit(); @@ -199,13 +199,13 @@ LIGHTWEIGHT_SQL_MIGRATION(20260819000013, "Create ledger_applied_ops table") { } LIGHTWEIGHT_SQL_MIGRATION(20260819000014, "Store SubmitReport params with the job row") { - // The job row has to be self-describing now that the aggregation no - // longer runs inside SubmitReport's own call frame: the - // params used to be decoded on the caller's thread and captured into the - // posted lambda, so nothing needed to persist them. With the run moved - // to ledger::app::App's runner -- possibly in a different process, and - // certainly after a restart -- the row is the only record of what was - // asked for. + // The job row has to be self-describing, because the aggregation does not + // run inside SubmitReport's own call frame: it runs on + // ledger::app::App's runner, possibly in a different process and certainly + // after a restart, so the row is the only record of what was asked for. + // Decoding the params on the caller's thread and capturing them into a + // posted lambda would need no persistence -- and would tie the run to the + // process that accepted it. // // Nullable (AddNotRequiredColumn, not AddColumn) because SQLite cannot // add a NOT NULL column to a table that may already hold rows without a @@ -216,12 +216,11 @@ LIGHTWEIGHT_SQL_MIGRATION(20260819000014, "Store SubmitReport params with the jo } LIGHTWEIGHT_SQL_MIGRATION(20260819000015, "Record which principal owns each book") { - // Per-book ownership. Until this column existed the rung had - // no notion of whose book a `ledgers` row was: the signed-token check and - // the per-action empty-principal gate both held, and neither says *whose* - // book this is, so any authenticated principal could read, write and post - // into any book -- including one another principal had just created with - // `CreateLedger`. + // Per-book ownership. Without this column the rung has no notion of whose + // book a `ledgers` row is: the signed-token check and the per-action + // empty-principal gate both hold, and neither says *whose* book this is, so + // any authenticated principal could read, write and post into any book -- + // including one another principal had just created with `CreateLedger`. // // Nullable (AddNotRequiredColumn, not AddColumn) for the same reason // 20260819000014's `params_json` is: SQLite cannot add a NOT NULL column diff --git a/examples/ledger/src/models/budget_model.cpp b/examples/ledger/src/models/budget_model.cpp index 1fd2b272a..00d5652ee 100644 --- a/examples/ledger/src/models/budget_model.cpp +++ b/examples/ledger/src/models/budget_model.cpp @@ -163,19 +163,17 @@ BudgetId BudgetModel::execute(const CreateBudget& action) { throw NotFound{"CreateBudget: no such ledger or category"}; } // The named book, and the category's own book -- a budget joins the - // two, so owning one of them is not enough. The existence - // check above keeps its combined message; ownership is a separate - // refusal. + // two, so owning one of them is not enough. The existence check above + // keeps its combined message; ownership is a separate refusal. if (!db::bookIsReachableBy(ledgerRows.front(), ctx->principal)) { throw Forbidden{"CreateBudget: this book belongs to another principal"}; } db::requireOwnedParentBook(mapper, categoryRows.front().ledger.Value(), ctx->principal, "CreateBudget"); // ...and the category's book must *be* the named book, not merely be - // owned by the same principal. This is the site that - // decides which `categoryId` `execute(GetBudgetReport)` below fans its - // account lookup out over, so a budget filed under book one naming a - // book-two category is the one cross-book row with report - // consequences. + // owned by the same principal. This is the site that decides which + // `categoryId` `execute(GetBudgetReport)` below fans its account lookup + // out over, so a budget filed under book one naming a book-two category + // is the one cross-book row with report consequences. db::requireCategoryInBook(categoryRows.front().ledger.Value(), ledgerRows.front().id.Value(), "CreateBudget"); db::BudgetRecord budgetRow; budgetRow.ledger = ledgerRows.front(); diff --git a/examples/ledger/src/models/ledger_model.cpp b/examples/ledger/src/models/ledger_model.cpp index 8cf303c4f..8e5426c2e 100644 --- a/examples/ledger/src/models/ledger_model.cpp +++ b/examples/ledger/src/models/ledger_model.cpp @@ -677,9 +677,9 @@ CreateLedgerResult LedgerModel::execute(const CreateLedger& action) { Lightweight::DataMapper mapper; db::LedgerRecord ledgerRow; ledgerRow.name = Light::SqlAnsiString<128>{action.name}; - // The caller owns what it creates. This is the only place - // an owner is ever written: every other action reads it, and a book - // whose owner is NULL is one written before this column existed. + // The caller owns what it creates. This is the only place an owner is + // ever written: every other action reads it, and a book whose owner is + // NULL is one written before this column existed. ledgerRow.owner = Light::SqlAnsiString<64>{ctx->principal}; mapper.Create(ledgerRow); auto result = CreateLedgerResult{.id = LedgerId{static_cast(ledgerRow.id.Value())}}; @@ -702,11 +702,11 @@ AccountInfo LedgerModel::execute(const OpenAccount& action) { } Lightweight::DataMapper mapper; // The ledger row must already exist -- `execute(const CreateLedger&)` - // above is what creates one. Load it by primary key - // rather than fabricating a stub LedgerRecord, since - // BelongsTo assignment needs the real persisted parent (per - // polls::db::OptionRecord's own `opt.poll = poll;` usage, where `poll` - // is a row that has actually round-tripped through Create/Query). + // above is what creates one. Load it by primary key rather than + // fabricating a stub LedgerRecord, since BelongsTo assignment needs the + // real persisted parent (per polls::db::OptionRecord's own `opt.poll = + // poll;` usage, where `poll` is a row that has actually round-tripped + // through Create/Query). const auto ledgerRow = db::requireOwnedBook(mapper, action.ledgerId, ctx->principal, "OpenAccount"); db::AccountRecord accountRow; accountRow.ledger = ledgerRow; @@ -750,10 +750,10 @@ GetLedgerResult LedgerModel::execute(const GetLedger& action) { // A read is where the gap would be widest: without a principal check of // any kind, a second authenticated client could ask for -- and get -- // every account and balance in a book it has nothing to do with. - // It carries no EmptyPrincipalError gate, because - // it does not need one: an empty principal never matches a recorded - // owner, so it is refused here and admitted only for an unowned book, - // which is exactly what it could always reach. + // It carries no EmptyPrincipalError gate, because it does not need one: an + // empty principal never matches a recorded owner, so it is refused here and + // admitted only for an unowned book, which is exactly what it could always + // reach. static_cast(db::requireOwnedBook(mapper, action.ledgerId, db::currentPrincipal(), "GetLedger")); // Real balance per account: the sum of every leg posted against it, // computed in-model via Rational::operator+ (never a raw SQL SUM() -- @@ -770,8 +770,8 @@ ListTransactionsResult LedgerModel::execute(const ListTransactions& action) { Lightweight::DataMapper mapper; // Same gate, same reason, as execute(GetLedger) directly above: a listing // of a book's entries is a read of the book, so it goes through - // db::requireOwnedBook and needs no EmptyPrincipalError gate - // of its own -- an empty principal never matches a recorded owner. + // db::requireOwnedBook and needs no EmptyPrincipalError gate of its own -- + // an empty principal never matches a recorded owner. static_cast(db::requireOwnedBook(mapper, action.ledgerId, db::currentPrincipal(), "ListTransactions")); // The month bound, as a half-open UTC [start, end) over the stored epoch @@ -1100,9 +1100,9 @@ GetLedgerResult LedgerModel::execute(const UndoTransaction& action) { } // The book gate runs after the journal is resolved, not before it, so // the two "no such journal" refusals this action already had keep - // their exact wording and order. The journal names its own - // ledger and that has just been verified against the action's, so - // gating on it is gating on the book the action really reaches. + // their exact wording and order. The journal names its own ledger and + // that has just been verified against the action's, so gating on it is + // gating on the book the action really reaches. db::requireOwnedParentBook(mapper, originalJournalRow.ledger.Value(), ctx->principal, "UndoTransaction"); // A compensating entry names the entry it reverses, so "has this already @@ -1549,11 +1549,11 @@ GetReportStatusResult LedgerModel::execute(const GetReportStatus& action) { throw NotFound{"GetReportStatus: no such job"}; } const auto& row = jobRows.front(); - // A job id carries no ledgerId of its own, so the book this - // read reaches is the one the job row names. Gated after the job lookup so - // "no such job" keeps its wording, and like `execute(GetLedger)` this pure - // read needs no separate empty-principal gate: an empty principal matches - // no recorded owner. + // A job id carries no ledgerId of its own, so the book this read reaches is + // the one the job row names. Gated after the job lookup so "no such job" + // keeps its wording, and like `execute(GetLedger)` this pure read needs no + // separate empty-principal gate: an empty principal matches no recorded + // owner. db::requireOwnedParentBook(mapper, row.ledger.Value(), db::currentPrincipal(), "GetReportStatus"); return GetReportStatusResult{ .status = static_cast(row.status.Value()), @@ -1605,16 +1605,16 @@ void LedgerModel::setCategoryImpl(Lightweight::DataMapper& mapper, const SetCate const auto principal = db::currentPrincipal(); db::requireOwnedParentBook(mapper, accountRows.front().ledger.Value(), principal, "SetCategory"); db::requireOwnedParentBook(mapper, categoryRows.front().ledger.Value(), principal, "SetCategory"); - // Then *which* book: owning both is not the same as their - // being one book, and until this check existed a caller could file its own - // account in book two under its own category in book one. + // Then *which* book: owning both is not the same as their being one book, + // and until this check existed a caller could file its own account in book + // two under its own category in book one. // // Unreachable from the rule cascade, which is the other caller: that path // looks its category up with a `Where` on the triggering action's own // `ledgerId`, and its account is a leg account, which `accountInLedger` - // has already constrained to that same ledger. The two are - // therefore always one book there, and this refusal is a live gate only - // for the client-facing `execute(SetCategory)` above. + // has already constrained to that same ledger. The two are therefore always + // one book there, and this refusal is a live gate only for the + // client-facing `execute(SetCategory)` above. db::requireCategoryInBook(categoryRows.front().ledger.Value(), accountRows.front().ledger.Value(), "SetCategory"); accountRows.front().category = categoryRows.front(); mapper.Update(accountRows.front()); diff --git a/examples/ledger/tests/test_ledger_category_scope.cpp b/examples/ledger/tests/test_ledger_category_scope.cpp index 91457c511..8f00b11e0 100644 --- a/examples/ledger/tests/test_ledger_category_scope.cpp +++ b/examples/ledger/tests/test_ledger_category_scope.cpp @@ -20,9 +20,9 @@ // The rule these cases pin: an account and a category are the same book's, or // the link is refused; a budget's category is its own book's, or the budget is // refused. The refusal is `NotFound{": category does not belong to -// this ledger"}` -- `accountInLedger`'s own idiom, deliberately -// distinct from `": no such account or category"`, because a client -// that cannot tell them apart cannot tell a dead id from a mis-scoped one. +// this ledger"}` -- `accountInLedger`'s own idiom, deliberately distinct from +// `": no such account or category"`, because a client that cannot tell +// them apart cannot tell a dead id from a mis-scoped one. // // The last case is the negative control: a same-book link and a same-book // budget still succeed. Without it a guard that refused every link would pass diff --git a/examples/ledger/tests/test_ledger_list_transactions.cpp b/examples/ledger/tests/test_ledger_list_transactions.cpp index bbfbb0c9a..e428ded56 100644 --- a/examples/ledger/tests/test_ledger_list_transactions.cpp +++ b/examples/ledger/tests/test_ledger_list_transactions.cpp @@ -274,10 +274,10 @@ TEST_CASE("A listed id drives UndoTransaction and restores the exact balances", TEST_CASE("ListTransactions refuses a principal that does not own the book", "[ledger][list-transactions][ownership]") { - // The gate every other book-reaching read carries. A listing - // of a book's entries is precisely the read it exists for: without it, a - // second authenticated client learns every description and amount in a - // book it has nothing to do with. + // The gate every other book-reaching read carries. A listing of a book's + // entries is precisely the read it exists for: without it, a second + // authenticated client learns every description and amount in a book it has + // nothing to do with. morph::ladder::testkit::DbFixture fixture; ledger::LedgerModel model; diff --git a/examples/ledger/tests/test_ledger_model.cpp b/examples/ledger/tests/test_ledger_model.cpp index 1482d7fad..51193f72f 100644 --- a/examples/ledger/tests/test_ledger_model.cpp +++ b/examples/ledger/tests/test_ledger_model.cpp @@ -40,10 +40,10 @@ class ScopedPrincipal { } // namespace TEST_CASE("CreateLedger bootstraps a book the rest of the action surface can use", "[ledger][model]") { - // The whole point of `CreateLedger`: every step below goes - // through the action surface, so this is exactly what an out-of-process - // client can do against an empty database. No `mapper.Create` anywhere -- - // if this test ever needs one again, the bootstrap gap is back. + // The whole point of `CreateLedger`: every step below goes through the + // action surface, so this is exactly what an out-of-process client can do + // against an empty database. No `mapper.Create` anywhere -- if this test + // ever needs one again, the bootstrap gap is back. morph::ladder::testkit::DbFixture fixture; ledger::LedgerModel model; @@ -111,9 +111,9 @@ TEST_CASE("OpenAccount creates an account visible in GetLedger", "[ledger][model morph::ladder::testkit::DbFixture fixture; Lightweight::DataMapper mapper; // Seeds the `ledgers` row directly rather than dispatching `CreateLedger` - // (which has its own cases above): this case - // is about `OpenAccount`, and a direct row keeps it from failing for a - // reason that belongs to a different action. + // (which has its own cases above): this case is about `OpenAccount`, and a + // direct row keeps it from failing for a reason that belongs to a different + // action. ledger::db::LedgerRecord ledgerRow; ledgerRow.name = "Personal"; mapper.Create(ledgerRow); diff --git a/examples/ledger/tests/test_ledger_model_keys.cpp b/examples/ledger/tests/test_ledger_model_keys.cpp index c15226454..5e99a4907 100644 --- a/examples/ledger/tests/test_ledger_model_keys.cpp +++ b/examples/ledger/tests/test_ledger_model_keys.cpp @@ -55,8 +55,8 @@ constexpr std::int64_t kIds[] = {1, 7, 4294967297, 9007199254740993}; TEST_CASE("LedgerModel keeps std::int64_t as its PrimaryKey because its keyed actions carry two id types", "[ledger][model][key]") { // A regression guard: the key type is `std::int64_t` and has to stay one. - // `BRIDGE_MODEL_KEY` would have deduced - // `LedgerId` from `&OpenAccount::ledgerId`, which is the better default + // `BRIDGE_MODEL_KEY` would have deduced `LedgerId` from + // `&OpenAccount::ledgerId`, which is the better default // -- but `GetReportStatus` keys this same model on a `ReportJobId` // (report_dto.hpp), so no single strong id is *the* key type here. The // model therefore declares the raw scalar in its own body, which diff --git a/examples/ledger/tests/test_ledger_reports.cpp b/examples/ledger/tests/test_ledger_reports.cpp index 546e049b7..4d3d66e01 100644 --- a/examples/ledger/tests/test_ledger_reports.cpp +++ b/examples/ledger/tests/test_ledger_reports.cpp @@ -269,9 +269,9 @@ TEST_CASE("A submitted report stays Pending for as long as nothing runs it", "[l // The property that says the executor really is outside the model, and one // a thread-pool-owning model could not express at all: with no runner // anywhere in the process, a submitted job is stable at Pending rather than - // merely "not done yet". This is also - // exactly what a job outliving the process that accepted it looks like -- - // the row waits for whichever runner comes along next. + // merely "not done yet". This is also exactly what a job outliving the + // process that accepted it looks like -- the row waits for whichever runner + // comes along next. morph::ladder::testkit::DbFixture fixture; Lightweight::DataMapper mapper; ledger::db::LedgerRecord ledgerRow; diff --git a/examples/ledger/tests/test_report_presenter.cpp b/examples/ledger/tests/test_report_presenter.cpp index 5343d0689..ac2a67ae7 100644 --- a/examples/ledger/tests/test_report_presenter.cpp +++ b/examples/ledger/tests/test_report_presenter.cpp @@ -9,11 +9,11 @@ // It is also the only test in this rung that runs a client and a // `ledger::app::App` together, which is what the shipped deployment actually // is. That became load-bearing rather than incidental when the report job -// moved out of the model: `SubmitReport` now only writes a -// Pending row, so without an App ticking somewhere in the process the -// client's poller would poll a job that nothing will ever settle. Everything -// below is unchanged from when the model computed the report inside its own -// `execute()` -- the client cannot tell the difference, which is the point. +// moved out of the model: `SubmitReport` now only writes a Pending row, so +// without an App ticking somewhere in the process the client's poller would +// poll a job that nothing will ever settle. Everything below is unchanged from +// when the model computed the report inside its own `execute()` -- the client +// cannot tell the difference, which is the point. #include #include diff --git a/examples/lims/gui_lib/lims_qml_conversions.hpp b/examples/lims/gui_lib/lims_qml_conversions.hpp index 4169a9ed9..1a55e2524 100644 --- a/examples/lims/gui_lib/lims_qml_conversions.hpp +++ b/examples/lims/gui_lib/lims_qml_conversions.hpp @@ -42,9 +42,9 @@ namespace lims::gui { /// An id as the plain number QML rows and invokables carry, `-1` when -/// unengaged. Re-exported rather than redefined so this rung's -/// `-1` is the same named `kNoId` the other four rungs publish, and so the -/// name stays `lims::gui::idNumber` for the callers and tests that use it. +/// unengaged. Re-exported rather than redefined so this rung's `-1` is the same +/// named `kNoId` the other four rungs publish, and so the name stays +/// `lims::gui::idNumber` for the callers and tests that use it. using ::morph::ladder::gui::idNumber; /// @brief A `string_view` as a `QString`. diff --git a/examples/lims/include/lims/core/self_journal.hpp b/examples/lims/include/lims/core/self_journal.hpp index 17b0e28a7..aa8467a0f 100644 --- a/examples/lims/include/lims/core/self_journal.hpp +++ b/examples/lims/include/lims/core/self_journal.hpp @@ -170,9 +170,9 @@ class SelfJournal { // this rung records is *unstamped*, and a later replay against a // renamed field would decode it to a default and report the wrong // state confidently -- the failure this rung's README names as the - // ladder's open question. Empty for an action whose - // ActionTraits is hand-written; see docs/spec/journal/journal.md, - // "Payload schema fingerprint". + // ladder's open question. Empty for an action whose ActionTraits is + // hand-written; see docs/spec/journal/journal.md, "Payload schema + // fingerprint". entry.schema = ::morph::model::detail::actionPayloadSchema(); if (const auto* ctx = ::morph::session::current()) { entry.principal = ctx->principal; diff --git a/examples/lims/include/lims/dto/offline_dto.hpp b/examples/lims/include/lims/dto/offline_dto.hpp index 8a98ac535..7338b930e 100644 --- a/examples/lims/include/lims/dto/offline_dto.hpp +++ b/examples/lims/include/lims/dto/offline_dto.hpp @@ -138,8 +138,8 @@ struct QueuedCapture { /// at-most-once *whatever* delivered it — the queue, a re-dispatch, or a /// journal replay. The queue itself is documented not to enforce /// uniqueness, and the shipped implementations disagree about whether they - /// do anyway, so the enforcement has to live where - /// it is asked for: in the consumer. + /// do anyway, so the enforcement has to live where it is asked for: in the + /// consumer. OperationKey operationKey; /// @brief What to record. diff --git a/examples/lims/src/db/schema.cpp b/examples/lims/src/db/schema.cpp index 7ff71cfd5..e1fe2bf4f 100644 --- a/examples/lims/src/db/schema.cpp +++ b/examples/lims/src/db/schema.cpp @@ -8,12 +8,12 @@ namespace lims::db { void configure(const std::string& connectionString) { - // Nothing in Lightweight stops a pooled DataMapper from being - // returned with a transaction still open on it -- `DataMapperPool::Return` - // does no transaction cleanup, and the cost lands on the next, unrelated - // borrower as a 60s stall and a `database is locked` it did not cause. - // Installing the audit here, at the one point every rung's process - // configures its database, turns that into an abort at the leak. See + // Nothing in Lightweight stops a pooled DataMapper from being returned with + // a transaction still open on it -- `DataMapperPool::Return` does no + // transaction cleanup, and the cost lands on the next, unrelated borrower + // as a 60s stall and a `database is locked` it did not cause. Installing + // the audit here, at the one point every rung's process configures its + // database, turns that into an abort at the leak. See // examples/common/db/pool_transaction_audit.hpp. (void)::morph::ladder::db::installPoolTransactionAudit(); diff --git a/examples/lims/src/models/analysis_catalog_model.cpp b/examples/lims/src/models/analysis_catalog_model.cpp index 3b8ff9890..026735291 100644 --- a/examples/lims/src/models/analysis_catalog_model.cpp +++ b/examples/lims/src/models/analysis_catalog_model.cpp @@ -75,14 +75,13 @@ void applyBounds(db::AnalysisVersionRecord& row, const Action& action) { /// data" boundary the README predicts. /// /// What is no longer app-private is the part the framework has a vocabulary -/// for. `morph::forms::InstanceConstraints` carries the -/// version's precision and specification range into `x-decimalPlaces`, -/// `x-minimum` and `x-maximum` — the framework's own keys — and the same -/// declaration is what `SampleModel` checks a submitted reading against, so -/// the advertised bound and the enforced one cannot drift apart. The -/// superseded `x-versionDecimalPlaces` is gone with it: serving two precision -/// keys and leaving a renderer to guess which one was true was worse than -/// either alone. +/// for. `morph::forms::InstanceConstraints` carries the version's precision and +/// specification range into `x-decimalPlaces`, `x-minimum` and `x-maximum` — +/// the framework's own keys — and the same declaration is what `SampleModel` +/// checks a submitted reading against, so the advertised bound and the enforced +/// one cannot drift apart. The superseded `x-versionDecimalPlaces` is gone with +/// it: serving two precision keys and leaving a renderer to guess which one was +/// true was worse than either alone. /// @param version The version to render. /// @return The schema text a client renders. [[nodiscard]] std::string renderSchemaFor(const AnalysisVersionView& version) { diff --git a/examples/lims/tests/test_backend_matrix.cpp b/examples/lims/tests/test_backend_matrix.cpp index 0457c5d71..72a01d2d1 100644 --- a/examples/lims/tests/test_backend_matrix.cpp +++ b/examples/lims/tests/test_backend_matrix.cpp @@ -200,8 +200,8 @@ TEST_CASE("An offline capture replays through the bridge under its operator's ow // actually drives it: the client drains its own queue and re-dispatches // each item as an ordinary action through its authenticated `Bridge`. // That is what makes `QueuedCapture`'s `capturedBy` check mean anything — - // see this rung's README §7 decision for why the - // framework's own `onBackendChanged()` drain cannot carry a session. + // see this rung's README §7 decision for why the framework's own + // `onBackendChanged()` drain cannot carry a session. const auto mode = GENERATE(Mode::Local, Mode::LocalSingleThread, Mode::Socket); CAPTURE(mode); DbFixture fixture; diff --git a/examples/lims/tests/test_offline_capture.cpp b/examples/lims/tests/test_offline_capture.cpp index 8d14f48f8..d267e7f20 100644 --- a/examples/lims/tests/test_offline_capture.cpp +++ b/examples/lims/tests/test_offline_capture.cpp @@ -93,8 +93,8 @@ struct Lab { /// the model's own strand, where `session::current()` is null, and every item /// is then refused for want of a principal. That is pinned separately, in /// `test_backend_matrix.cpp`'s "onBackendChanged fires on switchBackend, and -/// fails closed with no session". What this -/// helper exercises is the *classification* logic — base-version comparison, +/// fails closed with no session". What this helper exercises is the +/// *classification* logic — base-version comparison, /// conflict flagging, at-most-once — which is the same code the supported /// re-dispatch path runs, and which `test_backend_matrix.cpp` also drives /// through a real `Bridge` in all three deployment modes. @@ -730,8 +730,8 @@ TEST_CASE("The durable queue dedups a re-enqueued operation where the in-memory // that the queue "never interprets, requires, or enforces uniqueness" on // the key, but the two shipped implementations do not agree about that. // The three shipped queues disagree about enqueue-time dedup; this test - // pins the divergence so a future change - // to either one is noticed here rather than in a lab. + // pins the divergence so a future change to either one is noticed here + // rather than in a lab. const auto path = freshQueuePath("dedup"); morph::offline::SqliteOfflineQueue durable{path}; morph::offline::InMemoryOfflineQueue volatileQueue; diff --git a/examples/lims/tests/test_result_entry.cpp b/examples/lims/tests/test_result_entry.cpp index 28f085166..807cc14a2 100644 --- a/examples/lims/tests/test_result_entry.cpp +++ b/examples/lims/tests/test_result_entry.cpp @@ -397,12 +397,12 @@ TEST_CASE("An over-precise reading is rejected, not silently retagged", "[lims][ // 1.23456 needs five decimals. morph's own `x-decimalPlaces` enforcement // *rounds* on its wire dispatch paths rather than retagging, so storage and - // display agree there. This rung is - // stricter: the governing precision is the analysis version's runtime - // decimalPlaces (schema-versioned data, not the compile-time - // Quantity), and rounding a reading the method cannot support - // would record a measurement the analyst never made. The model is called - // directly here, so no framework reconciliation runs in between. + // display agree there. This rung is stricter: the governing precision is + // the analysis version's runtime decimalPlaces (schema-versioned data, not + // the compile-time Quantity), and rounding a reading the + // method cannot support would record a measurement the analyst never made. + // The model is called directly here, so no framework reconciliation runs in + // between. CHECK_THROWS_AS(model.execute(lims::CaptureConcentration{.analysisVersionId = nitrate.versionId, .value = lims::Concentration{exact(123456, 100000, 5)}}), lims::ValidationError);