bank/gui + ci: clear the eleven bank-GUI sources' clang-tidy debt, and gate the Q_OBJECT header split that no build catches early (fixes #656, fixes #659) - #665
Conversation
Runner verificationI drove #659's close condition myself, on the real tree rather than through a fixture. Removing restored → Rejecting Fixture 7b is the part I would have missed. Four of the six split headers are credited to #656's zero is a measurement, not silence. 97 reproduced exactly on On the safety claim, which is stronger than argued. The branch rests on On the two filed issues#664 confirmed structurally — only the root #663 carries a reproduction ( Not verified by me: the 97→0 measurement itself and the three bank GUI test runs (both need the Qt configure), and the Emscripten leg — Worktrees pruned. Not merged: CI incomplete at hand-off. 🤖 Generated with Claude Code |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Runner: red on
|
aa84b12 to
0a04b0d
Compare
Runner verificationAmend integrity checked, not assumed: Folding the correction into the commit that made the false claim, rather than stacking a retraction on top, is the right shape — a PR that ships one commit asserting something and a second withdrawing it leaves the assertion in the history for The finding that changes the storyBoth scores are identical on That means the original false sentence was not carelessness. It was the predictable output of a local run that could not see the finding at all. #666 is the systemically important one, and I verified its premiseWith 3.16.0 clang-tidy computes the same 87/45 and then discards both as non-user code, so a bare local This undermines a class of evidence I have been accepting all session. Several lanes have reported "clang-tidy clean locally" and been contradicted by CI, and at least this one is explained: the local tool was blind, not lenient. And CI's Catch2 is unpinned — On the decision to suppress bothThe derivation is the part that earns it: And the lane reported a finding against its own work: case 2's directive is inert for this branch's diff — deleting it changes nothing here — so it separately proved the directive is armed, over a one-line diff on that case's lambda-bearing assertion. Writing "this directive is armed rather than decorative, which the diff that introduced it did not by itself show" into the reason is exactly the standard the previous wording failed. Not verified by me: the 87/45 measurements, the Catch2 3.5.3 reproduction, and the whole-file re-check of #656's 97 → 0 against the runner's Catch2 series — all need the Qt configure. Nor was 🤖 Generated with Claude Code |
…ixes #656) #657 added -DMORPH_BUILD_BANK_GUI=ON to the clang-tidy job's Configure step, which put eleven bank-GUI sources into compile_commands.json for the first time. clang-tidy-diff only reports on changed lines, so nothing went red -- the findings were waiting for whoever next edited one of those lines. Re-measured on 7ab4c7a before touching anything, clang-tidy 22.1.8 (ci.yml pins CLANG_VERSION: "22"), Qt 6.11.2, against the clang-tidy job's own configure flags and its own -extra-arg pair, cold build directory: 97 findings inside the eleven sources themselves 44 cppcoreguidelines-pro-bounds-avoid-unchecked-container-access 23 performance-unnecessary-value-param 9 misc-const-correctness 8 readability-identifier-length 2 readability-static-accessed-through-instance 2 readability-implicit-bool-conversion 2 readability-avoid-nested-conditional-operator 2 modernize-use-auto 1 readability-function-cognitive-complexity 1 readability-container-size-empty 1 cppcoreguidelines-pro-bounds-constant-array-index 1 concurrency-mt-unsafe 1 cppcoreguidelines-avoid-c-arrays + modernize-avoid-c-arrays (one site, two check names, which is why the issue's per-check list sums to 98) Exactly the count #656 filed on 4563aff, file-for-file and check-for-check. Same command after this commit: 0. The two big checks were two mechanical passes, as the ticket predicted: - 44 `map[QStringLiteral("k")] = v` on QVariantMap become `map.insert(QStringLiteral("k"), v)`. QMap::operator[] on a non-const map inserts a default and hands back a reference; insert() does the same lookup and assignment in one call, so this is the same map with no bounds-unchecked accessor in it. - 23 by-value continuation parameters become const references. Completion<T>::then takes std::function<void(const T&)>, so every one of these was copying a DTO out of a reference the caller already held. The residual 30 needed judgement, one at a time: - `id` parameters (5) are renamed for what they identify -- cardId, payeeId, accountId -- in the headers too. QML binds Q_INVOKABLE arguments positionally, so no .qml file sees this. - CardController's two nested conditional operators become one if/else-if over CardStatus with Cancelled as the fall-through, which is also what stopped the two `statusText`/`statusKind` chains being read twice. - main.cpp: setApplicationName and exec are static on QCoreApplication, so they are called that way and `app` becomes const; std::getenv is concurrency-mt-unsafe and becomes qgetenv, which the two seed variables twenty lines below already used; the `const char* names[5]` becomes a QStringList indexed with .at(), which removes the C-array pair and the non-constant array index together; two `if (window)` become explicit null comparisons. - Both of the behaviour test's TEST_CASEs carry a reasoned NOLINTNEXTLINE(readability-function-cognitive-complexity), and the whole argument for both sits above the first one. The check scores a whole Catch2 TEST_CASE body -- clang-tidy names them `dummyFunction72` and `dummyFunction76` -- and what it scores here is Catch2's assertion expansion rather than a branch thicket: REQUIRE/CHECK expand to a do-while around a try/catch with a `&&` in the loop condition, which the metric charges +1/+2/+1, four points per assertion. Measured with the clang-tidy job's own configure and its own -extra-arg pair, clang-tidy 22.1.8, threshold lowered to 1 so both cases report rather than only the one over: dummyFunction72 "MoveMoneyPage's picker ..." 87, 21 REQUIRE/CHECK dummyFunction76 "Main.qml confirms ..." 45, 11 REQUIRE/CHECK 21 x 4 = 84 and 11 x 4 = 44, so three points of the 87 and one of the 45 are the whole of what the tests' own shape contributes. Commenting a single CHECK out of the first case moves it 87 -> 83, so four-per- assertion is measured and not arithmetic. Both numbers are identical on 7ab4c7a: neither is a regression this commit introduced. An earlier revision of this commit suppressed only the second case and stated, in the tree, that the first "scores under the threshold and stays covered". That was never measured. It was read off the finding not being *reported*, which is a different thing: clang-tidy-diff surfaces a finding only when one of its notes lands on a changed line, and on 7ab4c7a none of the first case's notes was on one. Renaming the `balanceOf` lambda's parameter put a changed line under one of them, and the job went red with test_bank_gui_qml_behaviour.cpp:115:1: error: function 'dummyFunction72' has cognitive complexity of 87 (threshold 25) [readability-function-cognitive-complexity,-warnings-as-errors] Splitting the first case was the alternative, and it cannot reach the threshold. At four points an assertion, 25 allows six assertions per TEST_CASE; that case's prologue alone -- register a user, open two accounts, stand up a QQmlEngine, load MoveMoneyPage.qml, drive the picker onto the savings account -- is eight, so every fragment is over before it asserts anything of its own. Hoisting the prologue into a helper relocates the score rather than removing it, and morph#296's defect *is* the sequence (pick, deposit, still picked, deposit again, the money followed the label) that 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 bank test including ones not yet written, and rather than a NOLINTBEGIN/NOLINTEND span, which would cover whatever is added between them. Both reasons sit above their directive, not wrapped around it (#631). No new .clang-tidy anywhere, and examples/bank/tests/.clang-tidy is untouched: widening it would be #652's mistake one directory over. Verified: - 97 -> 0 in-source findings, same command, same build directory. The run still reports 1743 diagnostics in include/morph/** headers from these eleven TUs and zero clang-diagnostic-error, so the analysis is live rather than silently skipping the files. - Anti-vacuity: reinstating one `map[...] = ...` in PayeeController.cpp brings the finding straight back (`PayeeController.cpp:52:20: error: possibly unsafe 'operator[]' ...`), so the zero is a measurement and not an empty walk. - Builds clean under clang 22.1.8 (-Weverything -Werror, clang-debug) and under gcc (gcc-debug), both configured from empty with -DMORPH_BUILD_BANK_GUI=ON. - bank_gui_tests and bank_gui_qml_tests both pass, all three cases. - The 97 -> 0 re-checked whole-file against the runner's own Catch2 series (3.5.3 headers ahead of the workstation's on -isystem), because the workstation's Catch2 cannot see this particular check at all (see below): all eleven sources, full check set, 0 findings inside them and 0 clang-diagnostic-error. - The clang-tidy-diff gate itself, reproduced locally rather than inferred. clang-tidy-diff.py with this job's -path/-p1/-extra-arg set, over `git diff -U0 origin/master`, exits 1 with exactly the CI error quoted above before the two-directive correction, and exits 0 after it -- over the branch's whole diff, not just the one file. - Anti-vacuity for the second directive, which this branch's own diff does not exercise at all: with the directive deleted, the same gate over a one-line diff on that case's `REQUIRE(pumpUntil([&app] { ... }))` line reports `dummyFunction76 ... cognitive complexity of 45 (threshold 25)` and exits 1; with the directive back, that diff exits 0. A one-line diff on a non-lambda assertion line in the same case reports nothing either way, which is why the shipped diff never reached it. Not verified: the Emscripten leg. wasm-demo.yml builds bank_gui_wasm from these same controller sources and no Qt-WASM toolchain is installed here. Also not verified locally in the CI configuration exactly: reproducing the clang-tidy-diff failure needed Catch2 3.5.x headers on the include path to match the runner's apt `catch2`. With the workstation's Catch2 3.16.0 clang-tidy computes the same 87 and 45 but drops both findings as non-user code, so a bare local run of the gate is green on a diff CI fails. Filed as #666, with #667 for the Catch2 version the .clang-tidy copies record. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW
…ixes #659) AUTOMOC finds a Q_OBJECT header two ways -- beside a translation unit of the same basename, or named in a target's own source list -- and when neither holds it generates nothing and says nothing. The .cpp compiles, the static library archives, and the first signal is a linker error about a missing vtable in every leg that links the target. On #657 that was six red legs at once, the fastest at 4m03s. cmake/morph_add_rung.cmake describes this failure in its own comment on the _lib_headers glob, names the case it was diagnosed on (pastebin::app::App, "hit the moment ladder_pastebin_tests linked it") and gives the remedy. #652 hit it again anyway, in a different CMakeLists. A comment in a file you are not editing is not a control. scripts/check_qobject_moc_pairing.py is. For every tracked header carrying an AUTOMOC macro it requires one of three things, and each is checked rather than assumed: - a translation unit of the same basename in the same directory; - the header named in a target's source list, parsed out of the CMake corpus by balanced-paren command extraction, with `#` comments blanked first (examples/common/CMakeLists.txt names fault_proxy.hpp five times in the paragraph explaining why it is listed; counting those would let the prose about the coverage stand in for the coverage) and FILE_SET argument blocks dropped (morph_qt's installed-header set names qt_websocket_server.hpp but drives no moc; morph_qt_impl's source list is what does); - the header under examples/<rung>/include/ for a rung in examples/rungs.txt -- the morph_add_rung() glob. This gate does not resolve CMake globs, so it asserts that one instead: it fails if morph_add_rung.cmake stops carrying a `file(GLOB_RECURSE _lib_headers ... include/*.hpp)` whose result reaches an add_library(). Four of the tree's six split headers are covered by nothing else. Measured on this tree: 349 tracked headers, 41 CMake files, 40 headers carrying an AUTOMOC macro -- 34 paired, 2 listed, 4 globbed, 0 uncovered. ## Why a text scan, and why drift-guard.yml #659 expected a gate over a configured build tree, as scripts/check_automoc_includes.sh is, and flagged the scoping problem: a build tree only holds what its configure enabled, so "every Q_OBJECT header must have moc output" false-positives on everything behind an off-by-default option -- the WASM shells, bank's GUI, every rung at MORPH_BUILD_LADDER=OFF. Checking the pairing rather than the output dissolves that. A header behind an off-by-default option still has to be listed in its conditionally-added target; which options a configure turned on does not enter into it. So this needs no configure, no compiler and no Qt, and fits drift-guard.yml's stated contract ("Every job here is fast and dependency-free; none of them compiles anything") rather than sitting behind the slow legs it exists to pre-empt. It deliberately does not check that the target owning the source list has AUTOMOC on -- that is a second way to get no moc output, it has never happened here, and resolving target properties means a configure. Recorded in the script's header rather than left implicit. ## Both vacuity traps, closed The tree is clean today, so this gate ships already green and would never announce a broken scan on its own. - It prints what it examined -- headers walked, headers carrying a macro, and which mechanism covered each -- and exits 1 when the macro-bearing set is empty. A scan that stops recognising Q_OBJECT is a failure, not a pass. - `--self-test` drives nine fixtures, two of them mutations of this repository's real files: fixture 6b: ` testkit/fault_proxy.hpp` deleted from morph_ladder_testkit's source list in a copy of the real examples/common/CMakeLists.txt -- the exact #652 regression, and the close condition #659 names. The gate reports `examples/common/testkit/fault_proxy.hpp` and exits 1. fixture 7b: the `file(GLOB_RECURSE _lib_headers ...)` line deleted from a copy of the real cmake/morph_add_rung.cmake, with a rung header credited to it. The gate exits 1 rather than keeping the credit. Each mutation asserts that it changed something, so a rename upstream turns the self-test red instead of quietly making it a no-op. The other seven cover the paired arm, the listed arm, an unlisted split, a header "covered" only by a comment and a FILE_SET, and a tree with no macro headers at all. ## Verification status Reproduced locally on 7ab4c7a + the #656 commit, python 3, no build: $ python3 scripts/check_qobject_moc_pairing.py walked 349 tracked header(s) across 41 CMake file(s) 40 carry an AUTOMOC macro: 34 paired with a same-directory translation unit 2 named in a target's source list 4 under a ladder rung's include/, globbed by morph_add_rung() 0 with no moc pairing at all ok: morph_add_rung() globs include/*.hpp into ladder_<rung>_lib Q_OBJECT moc-pairing lint OK. $ python3 scripts/check_qobject_moc_pairing.py --self-test ... self-test OK: 9 fixture(s), including the #652 mutation of the real examples/common/CMakeLists.txt and a mutation of the real cmake/morph_add_rung.cmake. Not verified: the gate against the pre-fix revision d380895 itself. The mutation in fixture 6b reconstructs that state from the current file rather than checking the old one out, so it proves the gate fires on the shape, not that it would have fired on that commit's whole tree. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW
Runner: rebased onto
|
0a04b0d to
f498055
Compare
Two tickets, one commit each, branched from
7ab4c7a9.22951eb1— 97 unreported clang-tidy findings in the eleven bank-GUI sources morph#651 brings into reach #656: clear the 97 clang-tidy findings-DMORPH_BUILD_BANK_GUI=ONbrought into theclang-tidyjob's reach.aa84b126— No gate catches a Q_OBJECT header split from its TU; the first signal is a link error in six CI legs #659: a drift-guard gate that catches aQ_OBJECTheader split from its translation unit, before the link does.Both accepted as framed. Neither was rejected, and #659 was not concluded
wontfix— see "Why notwontfix" below.#656 — the bank GUI's clang-tidy debt
My own re-measurement
The ticket's 97 was measured on
4563aff3and had not been reproduced since. Re-measured here on7ab4c7a9, before touching anything — clang-tidy 22.1.8 (ci.ymlpinsCLANG_VERSION: "22"), Qt 6.11.2, theclang-tidyjob's own configure flags plus its own-extra-argpair, cold build directory:gui/controllers/CardController.cppgui/controllers/LoanController.cppgui/controllers/TransactionController.cppgui/controllers/AccountController.cppgui/controllers/PayeeController.cppgui/main.cpptests/gui/test_bank_qml_surface.cpptests/gui/test_bank_gui_qml_behaviour.cppgui/controllers/AppController.cppgui/BankClient.cpp,gui/controllers/BankController.cppPer check, before: 44
cppcoreguidelines-pro-bounds-avoid-unchecked-container-access, 23performance-unnecessary-value-param, 9misc-const-correctness, 8readability-identifier-length, 2 each ofreadability-static-accessed-through-instance/readability-implicit-bool-conversion/readability-avoid-nested-conditional-operator/modernize-use-auto, and one each ofreadability-function-cognitive-complexity,readability-container-size-empty,cppcoreguidelines-pro-bounds-constant-array-index,concurrency-mt-unsafe, and one site reported under bothcppcoreguidelines-avoid-c-arraysandmodernize-avoid-c-arrays(which is why the issue's per-check list sums to 98 while the finding count is 97).It did not come back empty, so the ticket stands rather than closing as an artefact of the filer's local Qt. The count is identical file-for-file and check-for-check to
4563aff3, which is itself a small result: nothing between the two revisions touched these sources.The two mechanical passes, then the residual
67 of 97 were two passes, as the ticket predicted:
map[QStringLiteral("k")] = von aQVariantMapbecomemap.insert(QStringLiteral("k"), v).QMap::operator[]on a non-const map inserts a default and returns a reference to assign through;insertdoes the same lookup and assignment in one call, so the map is byte-identical and no bounds-unchecked accessor remains.constreferences.Completion<T>::thentakesstd::function<void(const T&)>, so each of these was copying a DTO out of a reference the caller already held.The residual 30 needed judgement one at a time, and the reasoning is in the commit message. Briefly:
idparameters renamed for what they identify (headers too — QML bindsQ_INVOKABLEarguments positionally, so no.qmlsees it);CardController's two nested conditional operators replaced by oneif/else ifoverCardStatus; inmain.cpp,setApplicationName/execcalled statically soappcan beconst,std::getenv→qgetenv(which the two seed variables twenty lines below already used), and theconst char* names[5]→ aQStringListread with.at(), which removes the C-array pair and the non-constant index together.Cognitive complexity: two directives, and a correction to what the first revision of this PR claimed
The first revision of this PR suppressed
readability-function-cognitive-complexityon the second behaviourTEST_CASEand wrote, in the tree, that "the sibling case above scores under the threshold and stays covered". It does not, and never did.clang-tidy-diffwent red on exactly that sibling:Both cases are now measured, with the
clang-tidyjob's own configure and its own-extra-argpair, clang-tidy 22.1.8, threshold lowered to 1 so that both report rather than only the one over:MoveMoneyPage's picker keeps naming …dummyFunction72REQUIRE/CHECKMain.qml confirms a posted transaction …dummyFunction76REQUIRE/CHECKWhich story is true: it was already over, the earlier measurement missed it
The branch's edits inside the first case are a lambda-parameter rename,
QObject*→auto*, and an addedINFO. None of them moved the number. Checking outorigin/master's copy of the file into the same build and re-running gives the same two scores:(
75rather than76because Catch2 names these off__COUNTER__, and this branch adds one more macro expansion earlier in the file.)So the 87 is not new. What is new is that it is reported.
clang-tidy-difffilters findings to changed lines, andClangTidyDiagnosticConsumeraccumulates that filter over the finding and each of its notes — so a finding surfaces when any one of its notes lands on a changed line. Onorigin/masternone of this case's notes was on a changed line; renaming thebalanceOflambda's parameter put one there. The earlier claim was read off the finding not being reported, which is a different fact from it being under the threshold.What the 87 is made of
REQUIRE/CHECKexpand todo { … try { … } catch (…) { … } } while ((void)0, (false) && …)— a loop (+1), a handler (+2 at nesting level 1) and a&&(+1): four points per assertion. 21 × 4 = 84 and 11 × 4 = 44, so 3 of the 87 and 1 of the 45 are everything the tests' own shape contributes (the nested lambdas here, the six-controller range-forthere). Measured rather than arithmetic: commenting a singleCHECKout of the first case moves it 87 → 83.Why a directive rather than a split, and what the split would have cost
At four points an assertion, a threshold of 25 permits six assertions per
TEST_CASE. The first case's prologue alone — register a user, open two accounts, stand up aQQmlEngine, loadMoveMoneyPage.qml, drive the picker onto the savings account — is eight assertions, 32. Every fragment of a split carries that prologue, so no split of this case reaches the threshold; it would take four fragments and the prologue hoisted into a helper, and the helper then scores what the fragments no longer do. The score moves, it does not go away. 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.tests/.clang-tidymakes the same argument for the framework's own suite and subtracts the check there; this file argues it per case instead.Two per-case directives, each with its own measured reason, rather than:
examples/bank/tests/.clang-tidy— that would subtract the check from every bank test including the ones not written yet, which is examples/common/testkit/.clang-tidy also governs the testkit *library* sources, contradicting its own prose #652's mistake one directory over. Still no new.clang-tidyanywhere and that file is still untouched;NOLINTBEGIN/NOLINTENDspan — which would also cover whatever is added between them;Each reason sits above its directive, never wrapped onto it (#631);
scripts/check_nolint_directives.shpasses with 173 directives, all annotating code.Verified, including the second directive's anti-vacuity
The
clang-tidy-diffjob was reproduced locally, not inferred:clang-tidy-diff.pywith this job's-path/-p1/-extra-argset, overgit diff -U0 origin/masterfor the branch's whole diff, exits 1 with exactly the CI error above before this correction and exits 0 after it.The second directive turns out not to be exercised by this branch's own diff at all — with it deleted, the full branch diff still reports only the 87. It is armed rather than decorative, which was checked separately: over a one-line diff on that case's
REQUIRE(pumpUntil([&app] { … }))line, deleting the directive reportsdummyFunction76 … 45 (threshold 25)and exits 1, and restoring it exits 0. A one-line diff on a non-lambda assertion line in the same case reports nothing either way — the gate's condition is a changed line carrying a user-code note, not merely a changed line inside an over-threshold case.The
97 → 0claim was also re-checked whole-file against the runner's own Catch2 series rather than the workstation's: all eleven sources, full check set, 0 findings inside them and 0clang-diagnostic-error.Why the first revision's local run agreed with it — filed as #666
Reproducing any of this locally needed Catch2 3.5.x headers ahead of the workstation's on
-isystem. With the workstation's Catch2 3.16.0, clang-tidy computes the same 87 and 45 and then drops both findings as non-user code, so a localclang-tidy-diffrun is green on a diff CI fails, silently and in both directions a reviewer would look. That is filed as #666 (reproduced; the mechanism insideClangTidyDiagnosticConsumeris inferred and labelled as such). #667 is the smaller one it turned up:tests/.clang-tidyand its nine copies state the job installs Catch2 3.4.0, which cannot be right — 3.4.0 has nodummyFunctionNNto name.How this landed in the history
Folded into
22951eb1(#656) by amend rather than stacked on top, so the PR does not ship one commit asserting something false and a second retracting it; the commit message carries the same correction and the same measurements.aa84b126(#659) was replayed unchanged — the two commits touch disjoint files. Head is now0a04b0de.scripts/check_tidy_suppression_scope.shandscripts/check_rung_filters.shboth pass.The claim the branch's safety rests on
That
QVariantMap::insert(key, value)is indistinguishable frommap[key] = valuefor these 44 sites. Every key is a distinct string literal written once per map, so there is no overwrite case where the two could differ, and both insert-or-assign. If that is wrong, the QML sees different property bags and the GUI silently shows nothing — which is exactly why the three bank GUI tests, all of which read those bags back through a live QML engine, were run.Verification
include/morph/**headers from these eleven TUs and zeroclang-diagnostic-error, so the analysis is live rather than skipping the files. And reinstating onemap[...] = ...inPayeeController.cppbrings the finding straight back:clang-debug,-Weverything -Werror) and under gcc (gcc-debug), both configured from empty with-DMORPH_BUILD_BANK_GUI=ON. Both compilers becauselinux-all-featuresruns both.bank_gui_testsandbank_gui_qml_testspass, all three cases, underQT_QPA_PLATFORM=offscreen.wasm-demo.ymlbuildsbank_gui_wasmfrom these same seven controller sources (main.cppis not in that target) and no Qt-WASM toolchain is installed here. The changes are Qt Core only and introduce no new API, so the risk is low — but it is unverified, not low-risk-therefore-verified.#659 — no gate caught a
Q_OBJECTheader split from its TUscripts/check_qobject_moc_pairing.py, wired intodrift-guard.ymlas its own job (self-test first, then the gate).AUTOMOC finds a
Q_OBJECTheader two ways and only two: beside a translation unit of the same basename, or named in a target's own source list. When neither holds it generates nothing and says nothing; the.cppcompiles, the static library archives, and the first signal is a linker error about a missing vtable in every leg that links the target — six red legs on #657, the fastest at 4m03s.cmake/morph_add_rung.cmakedescribes this failure and names the case it was diagnosed on; #652 hit it again in a different CMakeLists anyway.For every tracked header carrying an AUTOMOC macro, the gate requires one of three mechanisms, each checked rather than assumed:
#comments blanked first (examples/common/CMakeLists.txtnamesfault_proxy.hppfive times in the paragraph explaining why it is listed, and counting those would let the prose about the coverage stand in for the coverage) andFILE_SETargument blocks dropped (morph_qt's installed-header set namesqt_websocket_server.hppbut drives no moc;morph_qt_impl's source list is what does);examples/<rung>/include/for a rung inexamples/rungs.txt—morph_add_rung()'s glob. This gate does not resolve CMake globs, so it asserts that one: it fails ifmorph_add_rung.cmakestops carrying afile(GLOB_RECURSE _lib_headers ... include/*.hpp)whose result reaches anadd_library(). Four of the tree's six split headers are covered by nothing else.Measured on this tree:
Why not
wontfix, and whydrift-guard.ymlis the right hostThe ticket's escape hatch was: if the check needs a configured build to know which headers are reachable, it does not fit
drift-guard.yml's contract ("Every job here is fast and dependency-free; none of them compiles anything"), and saying so is a legitimate outcome.It does not need one. The scoping problem the issue raised — a build tree only holds what its configure enabled, so an output check false-positives on everything behind an off-by-default option — applies to checking the moc output. Checking the pairing dissolves it: a header behind an off-by-default option still has to be listed in its conditionally-added target, and which options a configure turned on does not enter into it. So there is no configure, no compiler, no Qt, and the gate runs in under a second. The
wontfixcondition is not met.It also does not check that the target owning the source list has AUTOMOC on. That is a second way to get no moc output; it has never happened here, and resolving target properties means a configure. Recorded in the script's header rather than left implicit.
The mutation the issue asked for
Both vacuity traps are closed, and the tree is clean today, so the gate ships already green.
It prints what it examined — headers walked, headers carrying a macro, and which mechanism covered each — and exits 1 when the macro-bearing set is empty. A scan that stops recognising
Q_OBJECTis a failure, not a pass.--self-testdrives nine fixtures, two of them mutations of this repository's real files:fixture 6b — the issue's stated close condition. A copy of the real
examples/common/, withtestkit/fault_proxy.hppdeleted frommorph_ladder_testkit's source list in the realexamples/common/CMakeLists.txt. That is the exact examples/common/testkit/.clang-tidy also governs the testkit *library* sources, contradicting its own prose #652 regression:exit 1, naming
examples/common/testkit/fault_proxy.hpp.fixture 7b — the
file(GLOB_RECURSE _lib_headers ...)line deleted from a copy of the realcmake/morph_add_rung.cmake, with a rung header credited to it. Exits 1 rather than keeping the credit. This is what stops mechanism 3 above from being a decorative assertion.Each mutation asserts that it changed something, so a rename upstream turns the self-test red instead of quietly making it a no-op. The other seven fixtures cover the paired arm, the listed arm, an unlisted split, a header "covered" only by a comment and a
FILE_SET, and a tree with no macro headers at all.The claim this half rests on
That AUTOMOC has exactly two ways of finding a
Q_OBJECTheader, so a header satisfying neither gets no moc output. That is Qt's documented behaviour and it is what both in-tree remedies (morph_add_rung.cmake's glob,examples/common/CMakeLists.txt's explicit listing) are written against. If a third mechanism exists, this gate is over-strict — it would report a header that is in fact mocced, which is a false positive on a clean tree and would be found immediately, not a silent hole.Not verified
The gate against the pre-fix revision
d380895citself. Fixture 6b reconstructs that state from the current file rather than checking the old commit out, so it proves the gate fires on the shape, not that it would have fired on that commit's whole tree.Findings filed, not folded
Both found while clearing #656; neither belongs to either change.
1e30in any amount field is UB #663 —bankgui::fmt::parseMinorcasts an unboundeddoubletostd::int64_t. Every amount the GUI accepts goes through it, the QML fields have no validator, andQString::toDoubleaccepts1e30. Reproduced with UBSan on the extracted arithmetic:runtime error: 1e+32 is outside the range of representable values of type 'long', exit 1. No sanitizer leg coversbank_gui, so nothing would have caught it. Includes a second, explicitly weaker observation about+ 0.5rounding on the same line..clang-tidy'sHeaderFilterRegex: "include/morph/.*"discards every finding in everyexamples/**header, from every job. Reproduced: 49 distinct findings across 12 headers from five of the eleven bank-GUI TUs, against a database of 737 entries — a lower bound, and a small one. This is the same shape as clang-tidy-diff's configure omits the application ladder, so no rung is in the compile database it lints against #481/tests/lint/ fixtures are inside clang-tidy-diff's scope, and nothing tells the author that until CI is red #650/60 of 470 tracked C/C++ sources are outside clang-tidy-diff's reach, and 11 of them only because one CMake flag is missing #651/97 unreported clang-tidy findings in the eleven bank-GUI sources morph#651 brings into reach #656 one level down, and it is why bank GUI: parseMinor casts an unbounded double to int64_t;1e30in any amount field is UB #663 sat unreported.No
triage:label on either; that verdict is thetriage-issueskill's, not the author's.Gates run locally before pushing
check_automoc_includes.sh(againstbuild/clang-debug),check_tidy_suppression_scope.sh,check_rung_filters.sh,check_bidi_controls.py,check_workflow_job_banners.py,check_nolint_directives.sh,check_qobject_moc_pairing.pyand its--self-test, andclang-format --dry-run -Werrorover exactly the twelve files touched. All clean. The workflow YAML parses.Re-run after the cognitive-complexity correction:
check_tidy_suppression_scope.sh,check_nolint_directives.sh,check_qobject_moc_pairing.pyand its--self-test,check_bidi_controls.py, andclang-format --dry-run -Werrorover the one file touched. All clean.CI has not been consulted. This PR was pushed and handed back immediately; whatever check counts are visible on it are incomplete.
🤖 Generated with Claude Code
https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW