The gap
ci.yml's clang-tidy-diff is the only job in this repository that runs clang-tidy over the tree, and it is diff-scoped twice over: clang-tidy-diff.py analyses only the files named in the diff, and passes a -line-filter that drops every finding outside the diff's own line ranges.
The consequence is that a finding in a first-party file nobody has touched is reported by no job at all, and never becomes reportable until someone happens to edit that exact line. That is not a bug in the diff job — a whole-file -Werror lint over this tree could not be turned on in one step — but it is a coverage gap that nothing currently records, and it is the gap #664 was pointing at from one directory over. (#664 blamed the root HeaderFilterRegex; measured, that regex has no observable effect on this job at all — see the correction on #664.)
The bill, measured rather than estimated
So the question a whole-file leg has to answer is "how much is standing debt". I measured it while working #664, on 563502ab, clang 22.1.8, Qt 6.11.2, with the clang-tidy job's own configure flags from a cold build directory:
cmake --preset clang-debug -DMORPH_BUILD_NET=ON -DMORPH_BUILD_QT=ON \
-DMORPH_BUILD_FORMS_QML=ON -DMORPH_BUILD_OFFLINE_SQLITE=ON \
-DMORPH_BUILD_LOAD_TESTS=ON -DMORPH_BUILD_HMAC_EXAMPLES=ON \
-DMORPH_BUILD_FUZZERS=ON -DMORPH_BUILD_LADDER=ON \
-DMORPH_BUILD_BANK_EXAMPLE=ON -DMORPH_BUILD_BANK_GUI=ON
740 database entries, 423 distinct in-workspace translation units, 282 under examples/. clang-tidy was then run over all 423, --header-filter='.*' --extra-arg=-std=c++23 --extra-arg=-Wno-missing-include-dirs, one output file per TU, findings de-duplicated by (file, line, column, check):
distinct findings under --header-filter='.*' : 5577
of which in a main file (always shown) : 3010
of which in an included header : 2567
by bucket:
2930 first-party: examples/
1422 build tree (_deps, vendored, generated)
713 first-party: tests/
488 first-party: include/
22 first-party: src/
2 system/toolchain
So a whole-file leg over first-party code, with the vendored trees excluded, starts at roughly 4155 findings (5577 − 1422 vendored), of which 3010 are in .cpp main files and ~657 in first-party headers that today's HeaderFilterRegex also excludes. For scale: #646 was 84 findings and #656 was 97.
Headers only, by root:
633 findings in 150 header(s) examples/
488 findings in 34 header(s) include/
21 findings in 6 header(s) tests/
1 findings in 1 header(s) src/
Cost, measured on a 12-core Linux box: 423 TUs at -P 10 took ~20 minutes wall, the heaviest TUs dominated by include/morph/forms/forms.hpp (102 findings alone) and the ORM-bearing bank models. A four-core hosted runner pays more.
What a fix would have to settle, and two facts that constrain it
- Vendored code must be excluded explicitly, and an inclusive first-party regex will not do it. A header filter is an unanchored search over an absolute path, and
(include/morph|src|tests|examples)/ matches 608 headers under build/clang-debug/_deps — 244 glaze, 202 stdexec, 153 Lightweight — because FetchContent lays dependencies out as _deps/<name>-src/src/ and _deps/<name>-src/include/. The polarity that works is HeaderFilterRegex: ".*" with an ExcludeHeaderFilterRegex naming the build tree (/_deps/, /build/, _autogen/, /CMakeFiles/); it also cannot go stale when a rung is added, because it names nothing first-party. Verified behaviourally: on one bank model TU that exclusion takes vendored diagnostic lines from 1300 to 0.
- A whole-file leg cannot simply be
WarningsAsErrors: "*" on day one. 4155 findings is not a backlog anyone clears in a PR, and suppressing to green is the outcome this repository has filed issues about repeatedly. The plausible shapes are a non-blocking reporting leg, a ratchet on the count, or a per-directory opt-in — all of which are design decisions, not implementation ones, which is why this is filed rather than attempted.
Verification status
- Reproduced: every number above, on
563502ab, over 423 of 423 in-workspace TUs — a full sweep, not a sample. The 608-vendored-header figure is a direct os.walk over build/clang-debug/_deps matched against the regex. The 1300→0 exclusion figure is two clang-tidy runs on examples/bank/src/models/account_model.cpp.
- Not verified: the wall-clock cost on a CI runner (measured locally only); whether the 4155 figure is stable across clang minor versions; and whether any of the findings are false positives worth suppressing rather than fixing — I counted them, I did not read them.
- Weak: the "roughly 4155" figure subtracts the vendored bucket from the total, which assumes the exclusion above is what a real leg would use. With a different exclusion the number moves.
What would change the verdict
Close as invalid if a job is added that already lints unchanged first-party code and I missed it — I checked .github/workflows/* and scripts/* for clang-tidy and run-clang-tidy invocations and found only the diff job plus scripts/check_tidy_suppression_scope.sh, which drives a synthetic probe rather than the tree. Close as wontfix if the considered answer is that diff-scoped lint is the intended coverage level — in which case this issue is still worth keeping as the record of what that choice costs, because nothing else in the repository writes it down.
Found while working #664 (PR #676). Filed, not folded.
🤖 Generated with Claude Code
https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW
The gap
ci.yml'sclang-tidy-diffis the only job in this repository that runs clang-tidy over the tree, and it is diff-scoped twice over:clang-tidy-diff.pyanalyses only the files named in the diff, and passes a-line-filterthat drops every finding outside the diff's own line ranges.The consequence is that a finding in a first-party file nobody has touched is reported by no job at all, and never becomes reportable until someone happens to edit that exact line. That is not a bug in the diff job — a whole-file
-Werrorlint over this tree could not be turned on in one step — but it is a coverage gap that nothing currently records, and it is the gap #664 was pointing at from one directory over. (#664 blamed the rootHeaderFilterRegex; measured, that regex has no observable effect on this job at all — see the correction on #664.)The bill, measured rather than estimated
So the question a whole-file leg has to answer is "how much is standing debt". I measured it while working #664, on
563502ab, clang 22.1.8, Qt 6.11.2, with theclang-tidyjob's own configure flags from a cold build directory:740 database entries, 423 distinct in-workspace translation units, 282 under
examples/. clang-tidy was then run over all 423,--header-filter='.*' --extra-arg=-std=c++23 --extra-arg=-Wno-missing-include-dirs, one output file per TU, findings de-duplicated by (file, line, column, check):So a whole-file leg over first-party code, with the vendored trees excluded, starts at roughly 4155 findings (5577 − 1422 vendored), of which 3010 are in
.cppmain files and ~657 in first-party headers that today'sHeaderFilterRegexalso excludes. For scale: #646 was 84 findings and #656 was 97.Headers only, by root:
Cost, measured on a 12-core Linux box: 423 TUs at
-P 10took ~20 minutes wall, the heaviest TUs dominated byinclude/morph/forms/forms.hpp(102 findings alone) and the ORM-bearing bank models. A four-core hosted runner pays more.What a fix would have to settle, and two facts that constrain it
(include/morph|src|tests|examples)/matches 608 headers underbuild/clang-debug/_deps— 244 glaze, 202 stdexec, 153 Lightweight — because FetchContent lays dependencies out as_deps/<name>-src/src/and_deps/<name>-src/include/. The polarity that works isHeaderFilterRegex: ".*"with anExcludeHeaderFilterRegexnaming the build tree (/_deps/,/build/,_autogen/,/CMakeFiles/); it also cannot go stale when a rung is added, because it names nothing first-party. Verified behaviourally: on one bank model TU that exclusion takes vendored diagnostic lines from 1300 to 0.WarningsAsErrors: "*"on day one. 4155 findings is not a backlog anyone clears in a PR, and suppressing to green is the outcome this repository has filed issues about repeatedly. The plausible shapes are a non-blocking reporting leg, a ratchet on the count, or a per-directory opt-in — all of which are design decisions, not implementation ones, which is why this is filed rather than attempted.Verification status
563502ab, over 423 of 423 in-workspace TUs — a full sweep, not a sample. The 608-vendored-header figure is a directos.walkoverbuild/clang-debug/_depsmatched against the regex. The 1300→0 exclusion figure is two clang-tidy runs onexamples/bank/src/models/account_model.cpp.What would change the verdict
Close as
invalidif a job is added that already lints unchanged first-party code and I missed it — I checked.github/workflows/*andscripts/*forclang-tidyandrun-clang-tidyinvocations and found only the diff job plusscripts/check_tidy_suppression_scope.sh, which drives a synthetic probe rather than the tree. Close aswontfixif the considered answer is that diff-scoped lint is the intended coverage level — in which case this issue is still worth keeping as the record of what that choice costs, because nothing else in the repository writes it down.Found while working #664 (PR #676). Filed, not folded.
🤖 Generated with Claude Code
https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW