ci: split mutation_survivors.json's citations by what they are, and fold the NOLINT gate into drift-guard (fixes #613, fixes #633) - #635
Merged
Conversation
…613) The file carried two kinds of `line` with opposite semantics and nothing told them apart. #613's decision comment measured the split: 11 citation occurrences (8 distinct sites) under `classes` assert something about *current* code, and 20 sit in dated campaign records that describe the tree as it was when the campaign ran. Live half -> the structured shape. The two `representative_sites` lists and the boundary-comparison shape under `unasserted` now carry {file, line, source, reason} entries. scripts/check_mutation_survivors.py picks them up with NO change to it -- find_entries() walks the whole document by design -- and goes from 7 audited citations to 15 while its free-text count drops 31 -> 20. Six of the eight had drifted; the two backend.hpp metric sites have identical twins elsewhere in the file, so each entry records that a future drift will be reported as ambiguous rather than corrected, which is the right answer for them. Historical half -> pinned, not stripped. A line number plus a revision is a working pointer forever, so each dated section gains a `revision` and a `revision_provenance` that states how well it is established rather than asserting it: runs[0] 92366f6 INFERRED (parent of the commit that recorded it; no citations in it) runs[1] d5c455f VERIFIED (both citations resolve) runs[2] adfe8e5 VERIFIED (gh run view 34349442137 headSha; the run id was never a revision, which #613 assumed it was) false_positive_finding d5c455f VERIFIED for the four header citations, partial for the tests/ ones mechanism_confirmed 567168b PARTIAL, and says so: that campaign measured standalone programs, not this tree classification_2026_09_09 adfe8e5 VERIFIED (all 11 citations re-resolved) The gate still does not audit the historical half and still prints how many it left alone, so a green tick cannot read as "all citations checked". `_comment` now states which sections are which, and that refreshing a historical line number falsifies the record. The only change to check_mutation_survivors.py is inside its module docstring, which claimed 31 free-text citations and called them rot. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AbwhcguQFkhvVi2AH19sWk
#631 put it in a workflow of its own only because drift-guard.yml was held by the then-open #614. #614 has landed, so the reason is gone. The two steps move unaltered onto prose-lint, alongside the spec-citation, CI-clang-pin and Catch2-name scans, which are the same shape: fast, dependency-free text scans that compile nothing. The "WHY THIS IS ITS OWN WORKFLOW" paragraph is dropped and the rest of the header comment -- the hazard, and the deliberate anchoring residual -- comes with the steps. The check name changes from "Suppression guard / NOLINT directives that cannot take effect" to the prose-lint check of this workflow. That is safe here and was checked rather than assumed: `gh api repos/.../branches/master/protection` returns 404 "Branch not protected" and `gh api repos/.../rulesets` returns `[]`, so nothing lists a required check by name. prose-lint's own job name is left alone -- it was already a partial list of what the job runs, and renaming it would rename a check for no gain. Proof the gate still runs and still fails after the move: re-wrapping a real directive in offline/replay_ledger.hpp onto two lines makes the folded step exit 1 and name the file and line; reverting it returns "166 NOLINTNEXTLINE directive(s), all annotating code", exit 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AbwhcguQFkhvVi2AH19sWk
Rebasing this branch onto a8511aa shifted the two `side_channel_metrics` representative sites in `include/morph/core/backend.hpp`. Both entries anticipated exactly this and recorded that the gate would report them as ambiguous rather than print a corrected line, because each cites a statement that appears character-for-character twice: include/morph/core/backend.hpp:1121 is allowlisted by a source line that appears 2 times (lines [1215, 1234]), and none of them is 1121, so which one is meant is not decidable. Make the entry unambiguous. include/morph/core/backend.hpp:1285 is allowlisted by a source line that appears 2 times (lines [1379, 1424]), and none of them is 1285, so which one is meant is not decidable. Make the entry unambiguous. Resolved by reading the code at each candidate, which is what those notes ask for. 1215 is the emission inside `registerModel`; 1234 is the `registerModelShared` arm, and the entry names `registerModel`. 1379 is the increment side -- `fetch_add`, `inFlightAfterInc`, outside the posted task; 1424 is the decrement twin inside it, and the entry names the increment side. Neither `source` text nor `reason` is touched, so no triage is re-stated that nobody performed. After: `python3 scripts/check_mutation_survivors.py` exits 0 with "15 structured citation(s) ... resolve to the line they name". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
This was referenced Sep 20, 2026
Yaraslaut
added a commit
that referenced
this pull request
Sep 20, 2026
…anch moved `scripts/branch_partial_allowlist.json` is keyed on `source` text, with `line` as a hint the gate audits — a drifted hint fails `check_branch_coverage.py`'s `resolve_allowlist_source_line` with "has moved to line N", by design (morph#349/#355/#419). Three of its entries sit below insertions this branch made: include/morph/core/backend.hpp:1324 -> 1414 include/morph/core/bridge.hpp:1542 -> 1551 include/morph/core/bridge.hpp:1654 -> 1673 The B11 entry's own prose cites the two textually-identical guards it is *not* about (`if (deadlineHandle && schedulerRef)` in the `.then`/`.onError` continuations); those moved 1678/1767 -> 1697/1786 and are repointed in the reason text too, since a citation inside a reason rots exactly like one in a field. Verified by resolving each entry's `source` against the working tree: all 22 entries match their hint exactly, and the three lines above were each read at their new number to confirm they are the same site the reason describes — the `catch` arm of `executeVia`'s dispatch guard for B11, not one of the two continuations. No entry's disposition changed. Not verified by a coverage run: no llvm-cov build was made here, so "still partial" is what the gate last measured, not something re-measured now. Separate commit from the three ticket commits on purpose: it is bookkeeping for insertions those commits made, and it must survive one of them being dropped. **Not repointed, and it will fail CI's mutation leg:** `scripts/mutation_survivors.json` needs `include/morph/core/backend.hpp:1322 -> 1412` (`aware.reserve(_changeAware.size());`), reported by `python3 scripts/check_mutation_survivors.py`: scripts/mutation_survivors.json: 1 citation(s) no longer match the code they name. - include/morph/core/backend.hpp:1322 has moved to line 1412. The text still matches, so nothing is wrong with the disposition -- update the `line` hint. That file is held by PR #635, so this lane does not touch it. Whoever lands these two branches applies that one-line hint change in whichever merges second. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW
Yaraslaut
added a commit
that referenced
this pull request
Sep 20, 2026
This branch adds 97 lines to `include/morph/core/backend.hpp`, which moves
every `side_channel_metrics` citation in it. The gate reported all three:
- include/morph/core/backend.hpp:1322 has moved to line 1412. The text still
matches, so nothing is wrong with the disposition -- update the `line` hint.
- include/morph/core/backend.hpp:1215 is allowlisted by a source line that
appears 2 times (lines [1305, 1324]), and none of them is 1215, so which
one is meant is not decidable. Make the entry unambiguous.
- include/morph/core/backend.hpp:1379 is allowlisted by a source line that
appears 2 times (lines [1469, 1514]), and none of them is 1379, so which
one is meant is not decidable. Make the entry unambiguous.
The first resolves mechanically (`aware.reserve(_changeAware.size());`,
1322 -> 1412). The other two are the pair whose entries record in advance that
a drift here is reported as ambiguous rather than auto-corrected, because each
cites a statement appearing twice verbatim, so each was resolved by reading the
code at both candidates:
1305 is the `registerCount` emission inside `registerModel`; 1324 is the
`registerModelShared` arm. The entry names `registerModel`.
1469 is the `executeInFlight` increment -- `fetch_add`, `inFlightAfterInc`,
outside the posted task; 1514 is the decrement twin inside it. The entry
names the increment side.
No `source` text and no `reason` is touched, so no triage is re-stated that
nobody performed. After: `python3 scripts/check_mutation_survivors.py` exits 0.
This became possible only once #635 landed: that PR held this file, so the
repoint could not ride the commits that caused it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW
Yaraslaut
added a commit
that referenced
this pull request
Sep 20, 2026
…e adapter's own completions, and document TimeoutScheduler::cancel (fixes #615, fixes #619, fixes #620) (#639) * core: say what TimeoutScheduler::cancel() does about an already-firing callback (fixes #620) `cancel()`'s `@brief` said "immediately" and its body said "a no-op if the handle already fired or was already cancelled". Neither covers the third case, which is the one a caller reasoning about lifetime cares about: `run()` erases the entry *before* invoking the callback and drops `_mtx` across the invocation, so a `cancel()` for a callback that has started takes the not-found branch and returns while that callback is still running on the scheduler thread. "Already fired" silently covered both "finished" and "currently running". This is documentation plus one contract clarification, not a behaviour change. Making `cancel()` wait would be the wrong fix -- a callback that posts back to the cancelling thread would deadlock it, the family `docs/spec/concurrency_and_lifetimes.md` names and the reason `CallbackScope` has no block-until-drained either. What was missing is the obligation that non-waiting places on a caller, so `cancel()` now states it: every scheduled callback must stay safe to run after its own `cancel()`. - `cancel()` (threaded build) distinguishes "not started" from "already started", says `~TimeoutScheduler` is the only thing here that means "no callback in flight", and names why blocking is not the alternative. - The `@file` comment gains the third build difference. Only the browser build, being single-threaded, actually gives "no callback starts after `cancel()` returns"; the bullet list previously documented the strictly less interesting timer-not-cleared difference and omitted this one. - The browser `cancel()` says its "already fired" can only mean *finished*, and that a caller wanting both builds still gets the weaker guarantee. - Both shipped callers now say why a late callback is harmless rather than leaving it to be rediscovered: `Bridge::executeVia`'s deadline relies on `CompletionState`'s first-result-wins, `RemoteServer`'s `executeTimeout` on `complete`'s reply-exactly-once flag. - `docs/spec/core/completion.md` said "Two behavioural differences"; it is three now, and it names which one a cross-build caller must code against. Verification: two Catch2 cases added. "cancel() returns while the callback it names is still running" reproduces the finding (measured, GCC 16.2.1 Debug, Linux); its contrast case asserts `~TimeoutScheduler` *does* join, which is what makes the first a real distinction rather than a timing accident. Mutated to confirm the first case is not vacuous: running the callback under `_mtx` in `run()` (so `cancel()` blocks behind it) fails it at `REQUIRE_FALSE(finished.load())` with `!true`. Not verified: the Emscripten build, which no toolchain here can compile -- its stronger guarantee is inferred from being single-threaded, as the header's own `@warning` already says of everything in that branch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW * core: let SynchronousBackendAdapter::cancelPending cancel the completions it produced (fixes #619) `cancelPending` was a one-line forward to the wrapped backend. The two verbs this adapter exists to reshape do not produce completions the wrapped backend knows about: `bindModel`/`promoteModel` settle from a task posted to the adapter's own `_control` strand, holding a promise `_inner` has never seen. So the forward reached none of them, and a bind cancelled by `~Bridge` or by `switchBackend` went on to resolve **successfully** afterwards -- the exact opposite of the contract it overrides ("after this call, any later `setValue`/`setException` on those states is a no-op, so in-flight server replies cannot resurrect a cancelled completion"). The adapter now keeps a `weak_ptr` to each dispatched promise and rejects the live ones before forwarding, on the snapshot-then-deliver shape and the amortised compaction `LocalBackend::cancelPending`/`trackPending` already use (morph#528). An entry expires when its strand task is destroyed, so the success path erases nothing; tracking happens before the post, so a cancellation landing in the gap still finds the promise. Two limits are stated in the header rather than left to be rediscovered. A task that settles first wins -- its completion was not still pending, the same race `LocalBackend` has always had. And a task already queued on `_control` still runs its blocking control call against the wrapped backend after the cancellation: the caller is told the bind was cancelled while the registration may still go through. Stopping that needs the task to check before calling `op()`, not the promise to be settled after it, so it is a separate change, filed as morph#636 rather than folded in here. Verification: reproduced and then fixed, measured on GCC 16.2.1 Debug, Linux. The new case wraps a backend whose control call blocks until the test releases it, so the completion is provably still pending when `cancelPending` runs, and covers `bindModel` and `promoteModel` in two sections -- morph#619 had measured only the bind and inferred the promote. Mutated to confirm it is not vacuous: restoring the plain `_inner->cancelPending(exc)` forward fails it in both sections at `REQUIRE(waitUntil([&] { callerExec.runOnce(); return errRan.load() == 1; }))` with `false`, i.e. no rejection is ever delivered. Not verified: any production effect, because there is none to have -- `grep -rn SynchronousBackendAdapter` still finds no production call site, and morph#571 making this surface the default path is what would turn this from latent into live. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW * core: put switchBackend and the reconnect handler on bindModel, and let them ask bindWaitPolicy (fixes #615) After morph#568 and morph#593, `Bridge` had four places that acquire a model for a binding. Two went through the structural surface and honoured `IBackend::bindWaitPolicy()`; the other two -- `switchBackend`'s phase 1 and `installReconnectHandler`'s reconnect callback -- still called the blocking `registerModelShared`/`registerModelWithContext` directly, with no policy check and no structural path at all. So a backend that had just been given a way to say "do not block my caller" was blocked at both of them anyway: a WASM client that never blocks on its *first* registration still could on a reconnect or a backend swap, where the block is a nested `QEventLoop` on the very thread that has to deliver the reply. Both now dispatch through `bindModel` with the shape every other site already has -- dispatch, park an inline reply in an `AsyncDispatchHandoff`, then `awaitHandoff` or `claimHandoff` according to the policy. For every `kCallerMayBlock` backend in the tree that is behaviour-for-behaviour what it was: the default `bindModel` runs exactly the legacy verb the request shape names, settles inside the call, and the wait finds the outcome already there. What is new is what happens when the answer is `kCallerMustNotBlock`: - `switchBackend` defers those binds. The swap happens, each deferred binding's `currentId` is cleared, `registrationInFlight` is set so `whenBound()` can gate on the window, and the reply publishes its own id. Atomicity is now exactly as strong as the wait is, and the header and bridge.md both say so rather than continuing to promise all-or-nothing: "did every re-registration succeed" is not knowable without waiting, and waiting is the deadlock. A deferred bind that fails after the swap is logged and rejects that binding's waiters; it cannot roll the switch back. - The rollback keys on a rejected `Completion` instead of a thrown exception, which is what morph#615 asks for: the structural surface reports failure through the completion, and a bare `catch (...)` cannot see it. - Waiters are resolved, and the staging exception rethrown, only after `_mtx`/`_attachMtx` are released -- resolving a waiter runs consumer code that is free to re-enter the `Bridge`. - The reconnect handler clears `currentId` for a deferred binding: the id belonged to the connection that just dropped, so `executeVia`'s fast fail is the honest answer rather than dispatching a dangling id. A failing re-registration there no longer throws out of the handler onto the transport thread and abandons every binding after it; it is reported per binding and the loop continues. The three sites' continuation pair is now one `makeBindCallbacks` body rather than three copies of the same liveness gate, stale-backend check and waiter settlement -- they had three chances to solve it differently. The per-binding dispatch is likewise one `rebindThroughSurface`, and `switchBackend`'s two phases are `stageRebinds`/`rollbackStaged`/`commitRebinds`. That last split is not tidiness: with everything inline, clang-tidy's `readability-function-cognitive-complexity` scored `switchBackend` at 45 and `installReconnectHandler` at 46 against a threshold of 25 -- CI's clang-tidy-diff leg failing -- and it was right. A staging phase, a rollback, a commit and a settlement pass in one body is four concerns the reader has to hold at once. Also settles the ownership disagreement morph#615 reports as its second finding: docs/spec/core/backend.md's migration table assigned the reconnect half to morph#570, whose own body scopes itself to `examples/` and never mentions `bridge.hpp`, and assigned `switchBackend` to nobody. The table now has a morph#615 row and morph#570 keeps only what its body claims. Verification, measured on GCC 16.2.1 Debug, Linux: full suite 1547 cases / 22704 assertions, all passing (the single "failed as expected" case is test_replay_ledger's deliberate negative-conformance run). Three new cases, each run against a double whose `bindModel` never settles on its own and whose legacy verbs park the calling thread, so a site that goes back to a blocking verb does not merely fail an assertion -- it fails to return. Each runs the site on its own thread, records whether it came back inside the polling budget, releases the latch so the thread is joinable either way, and asserts afterwards, so a regression fails the case instead of wedging the suite. Also run: clang-tidy-diff over the whole branch diff against a configure-only clang-debug database (clang 22.1.8, matching CI's pinned CLANG_VERSION), which now reports nothing. Partial rather than equivalent to CI: that database is configured with tests only, where CI configures every optional feature, so a translation unit this run never analysed could still reach a changed line. Mutated twice. Re-introducing the blocking legacy call at both sites, leaving everything else intact, fails both timing cases at `REQUIRE(returnedPromptly)` with `false`. Building the new tests against this branch's base (`git show a8511aa:include/morph/core/bridge.hpp`) fails all three, including the rollback case at `REQUIRE_THROWS_AS(...)` with "no exception was thrown where one was expected" -- the old loop never called `bindModel`, so a rejection it never asked for could not reach it. Worth recording because it nearly slipped through: the *first* mutation run passed. The double's legacy verbs were bounded by `kDefaultWaitBudget`, the same two seconds the test's own `waitUntil` polls for, so the blocked thread came back at almost exactly the moment the measurement gave up. The bound is now 60 s -- a wedge guard, not a race partner -- and only then did the mutation fail the cases. A budget equal to the thing it measures is not a measurement. Not verified: any of this against a real `QtWebSocketBackend` with `asyncRegistrationEnabled`, or in a browser. No WASM toolchain is available here, and no shipped example sets that flag *and* relies on reconnect or `switchBackend`, so the WASM consequence remains inferred from the dispatch chain exactly as morph#615 states it -- what is measured is that neither site blocks a caller on a `kCallerMustNotBlock` backend, and that both bind it correctly when the reply lands. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW * gates: repoint the three branch-coverage allowlist line hints this branch moved `scripts/branch_partial_allowlist.json` is keyed on `source` text, with `line` as a hint the gate audits — a drifted hint fails `check_branch_coverage.py`'s `resolve_allowlist_source_line` with "has moved to line N", by design (morph#349/#355/#419). Three of its entries sit below insertions this branch made: include/morph/core/backend.hpp:1324 -> 1414 include/morph/core/bridge.hpp:1542 -> 1551 include/morph/core/bridge.hpp:1654 -> 1673 The B11 entry's own prose cites the two textually-identical guards it is *not* about (`if (deadlineHandle && schedulerRef)` in the `.then`/`.onError` continuations); those moved 1678/1767 -> 1697/1786 and are repointed in the reason text too, since a citation inside a reason rots exactly like one in a field. Verified by resolving each entry's `source` against the working tree: all 22 entries match their hint exactly, and the three lines above were each read at their new number to confirm they are the same site the reason describes — the `catch` arm of `executeVia`'s dispatch guard for B11, not one of the two continuations. No entry's disposition changed. Not verified by a coverage run: no llvm-cov build was made here, so "still partial" is what the gate last measured, not something re-measured now. Separate commit from the three ticket commits on purpose: it is bookkeeping for insertions those commits made, and it must survive one of them being dropped. **Not repointed, and it will fail CI's mutation leg:** `scripts/mutation_survivors.json` needs `include/morph/core/backend.hpp:1322 -> 1412` (`aware.reserve(_changeAware.size());`), reported by `python3 scripts/check_mutation_survivors.py`: scripts/mutation_survivors.json: 1 citation(s) no longer match the code they name. - include/morph/core/backend.hpp:1322 has moved to line 1412. The text still matches, so nothing is wrong with the disposition -- update the `line` hint. That file is held by PR #635, so this lane does not touch it. Whoever lands these two branches applies that one-line hint change in whichever merges second. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW * ci: repoint three backend.hpp citations this branch moved This branch adds 97 lines to `include/morph/core/backend.hpp`, which moves every `side_channel_metrics` citation in it. The gate reported all three: - include/morph/core/backend.hpp:1322 has moved to line 1412. The text still matches, so nothing is wrong with the disposition -- update the `line` hint. - include/morph/core/backend.hpp:1215 is allowlisted by a source line that appears 2 times (lines [1305, 1324]), and none of them is 1215, so which one is meant is not decidable. Make the entry unambiguous. - include/morph/core/backend.hpp:1379 is allowlisted by a source line that appears 2 times (lines [1469, 1514]), and none of them is 1379, so which one is meant is not decidable. Make the entry unambiguous. The first resolves mechanically (`aware.reserve(_changeAware.size());`, 1322 -> 1412). The other two are the pair whose entries record in advance that a drift here is reported as ambiguous rather than auto-corrected, because each cites a statement appearing twice verbatim, so each was resolved by reading the code at both candidates: 1305 is the `registerCount` emission inside `registerModel`; 1324 is the `registerModelShared` arm. The entry names `registerModel`. 1469 is the `executeInFlight` increment -- `fetch_add`, `inFlightAfterInc`, outside the posted task; 1514 is the decrement twin inside it. The entry names the increment side. No `source` text and no `reason` is touched, so no triage is re-stated that nobody performed. After: `python3 scripts/check_mutation_survivors.py` exits 0. This became possible only once #635 landed: that PR held this file, so the repoint could not ride the commits that caused it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Yaraslaut
added a commit
that referenced
this pull request
Sep 20, 2026
…idy silences in headers (fixes #621, fixes #632) (#638) * ci: move seven job banners onto the jobs they describe, and gate it (fixes #621) Seven of ci.yml's seventeen `# ── … ──` section banners sat above the *previous* job's trailing `sccache stats` / `Save sccache` steps, so each banner -- and for several jobs the multi-paragraph argument under it, which is the only written record of why that job exists -- described a job beginning 6-31 lines further down, behind another job's steps. `# ── Valgrind (memcheck) ──` was followed by three `linux-all-features` steps and only then by `valgrind:`. The move is comment-only. Verified against the base revision: banners old/new: 17 17 same multiset: True non-comment lines identical & in order: True 1133 1133 comment multiset identical: True 1173 1173 parsed YAML identical: True jobs: 18 so no job, step, key or expression changed -- only which lines the comments sit between. A displacement check on its own would have been vacuous: a file with every banner *deleted* scores a perfect zero displacements, which is this repository's named failure mode. scripts/check_workflow_job_banners.py therefore pairs both ways -- every banner must introduce a job, and every job must be introduced by a banner -- so deleting a banner fails rule B instead of satisfying rule A. Four jobs carry no banner today and are listed in UNBANNERED with a reason; the two hygiene rules from check_workflow_option_coverage.py's EXEMPT (an entry for a job that does not exist is an error, an entry for a job that now has a banner is an error) keep that list from rotting. Workflows with no banner at all are skipped, derived from the file rather than listed. Measured, not asserted: - Run against the pre-fix ci.yml the gate reports the seven displacements at 321, 711, 877, 967, 1363, 1641 and 1919 -- exactly the set #621 filed -- plus the seven now-bannerless jobs, and exits 1. - Run against this tree it exits 0 over all 24 banners in ci.yml and drift-guard.yml. - scripts/test_check_workflow_job_banners.sh drives twelve mutations: the founding displacement reproduced mechanically, a banner deleted rather than moved, a new job added without a banner, both UNBANNERED hygiene rules, the banner syntax reworded tree-wide (anti-vacuity), and four false-positive mirrors. All twelve pass. - python3 scripts/check_workflow_option_coverage.py . still exits 0 with the new job present. drift-guard.yml is the more natural home for the job and is where it belongs once PR #635 lands; it is held there now, so the job lives in ci.yml -- the same arrangement the option-coverage job is in. drift-guard.yml's own six banners were measured and are all correctly placed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW * lint: record and gate the reach tests/.clang-tidy actually has (fixes #632) clang-tidy resolves its configuration from the path of the translation unit it analyses, not from the path of the file a diagnostic lands in. tests/.clang-tidy therefore scopes which TUs it governs, not which files their diagnostics may name: its thirteen suppressions -- each argued as Catch2 or raw-syscall idiom, correctly, about test code -- are also off for every include/morph/** header those TUs reach. A header gate driven by test TUs is green on findings it was built to catch. #632 confirmed the mechanism on one case and did not size it. Measured here, on a8511aa with clang-tidy 22.1.8, over the 134 tests/ TUs in the compile database the clang-tidy-diff job configures for itself (every optional feature ON; 703 entries, 134 distinct tests/ TUs, all 134 analysed, zero clang-diagnostic-error in either run): distinct findings inside include/morph/**, from those 134 TUs with tests/.clang-tidy in place .... 262 with it removed .................... 595 hidden by it ....................... 333, across 25 headers 247 cppcoreguidelines-pro-bounds-avoid-unchecked-container-access 57 readability-identifier-length 6 cppcoreguidelines-pro-bounds-array-to-pointer-decay 5 modernize-avoid-c-arrays 5 cppcoreguidelines-avoid-c-arrays 5 bugprone-unchecked-optional-access 3 cppcoreguidelines-pro-type-vararg 3 readability-convert-member-functions-to-static 2 readability-function-cognitive-complexity The set difference is one-directional: 0 findings appear with the file present that do not appear with it removed, which is what the mechanism predicts. The reach cannot be narrowed -- clang-tidy offers no way to scope a suppression to the file a diagnostic lands in -- so this takes #632's third close: write it down, and gate the record so it cannot go stale. - tests/.clang-tidy gains a section stating the reach, the measurement above, and a machine-readable `header-reach:` list naming all thirteen checks a test-TU-driven gate cannot report. - scripts/check_tidy_suppression_scope.sh checks both halves. Textually, that `header-reach:` names exactly what `Checks:` subtracts. Behaviourally, that the reach still exists: a probe header under include/morph/ is analysed from a TU under tests/ and from one that is not, using this repository's real .clang-tidy files, and the finding must be absent from the first and present from the second. The second probe is the anti-vacuity control -- the first assertion is a negative, which a probe that produces no findings at all satisfies by accident. Behavioural rather than a grep so that the note goes red rather than quietly stale if clang-tidy ever resolves configuration per diagnostic file. - Both run in the existing clang-tidy job, the only job that already has a pinned clang-tidy, before the Configure step: neither reads a compile database. Verified: - scripts/test_check_tidy_suppression_scope.sh drives nine cases -- a suppression added without a record entry, a record entry with no suppression, a suppression removed but recorded, the Checks: block reworded out from under the parser, the probed check ceasing to be suppressed, the probe rendered unable to fire from anywhere, and two false-positive mirrors. All nine pass. - The probe reproduces #632's mechanism independently of the sweep above: the same two checks are reported against include/morph/probe_scope.hpp from src/probe_scope_tu.cpp and not from tests/probe_scope_tu.cpp. On the nine examples/*/tests/ copies: #632 states they "carry the same entries". They do not -- each subtracts exactly one check, bugprone-chained-comparison. Their reach is the same but its effect is nil: that check fires only on Catch2's REQUIRE expansion and is attributed to the test source line. Measured over the 154 example test TUs in the same database, findings inside include/morph/** were identical with and without those nine files, 312 either way -- but that run cannot separate "no header reach" from "the check never fired", because Catch2 3.16.0 locally carries the NOLINT and bugprone-chained-comparison produced zero findings in either run. The structural argument carries the conclusion; the measurement only fails to contradict it. Recorded as such in tests/.clang-tidy, and noted on #632. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Sep 20, 2026
check_workflow_job_banners.py's docstring still names suppression-guard.yml, which #635 deleted
#643
Closed
Yaraslaut
added a commit
that referenced
this pull request
Sep 21, 2026
A bidirectional control renders as nothing and occupies no column, so a diff containing one looks exactly like a diff that does not. In a string literal that is a silently wrong assertion (#610: nine lines of tst_i18n.qml whose expected values held raw U+200E / U+200F / U+061C). In a comment it is source that stops saying what it means (#628 reproduced that on itself, twice, in two files, typing an escape through a JSON-payload tool that decoded it before the file was written). Between them is the trojan-source shape, where an override or isolate makes a line render as a different program from the one that compiles. #610 asked for this lint and deferred it as "a bigger decision". Review is not a control for this class -- the PR that closed #610 was reviewed by people who could not have seen a raw control had one survived -- so a gate is the only thing that can hold the line. scripts/check_bidi_controls.py rejects twelve codepoints in three classes, the list #610 wrote: U+061C, U+200E/U+200F, U+202A-U+202E and U+2066-U+2069. Comments are in scope, not excluded: #628's own reproduction was inside comments, and trojan source lives in comments by construction. The tree is already clean -- 0 raw controls across 1242 tracked files -- so the gate ships green and would be worth nothing unless every way of going blind is closed: - The detector is probed against each declared codepoint, one at a time, before any file is opened. A detect() narrowed to the two marks this repository has actually hit fails on a clean tree. - Scanning zero files is an error, not a pass. - scripts/test_check_bidi_controls.sh holds its own independent list of the twelve and requires each to be found in tests/lint/bidi_controls/. That is what notices a codepoint being deleted from the checker's table, which the checker's in-process probe cannot see, because both sides read it. - The diagnostic replaces each control with a visible <U+200E> marker, and the self-test asserts no diagnostic contains a raw control: a gate against invisible characters that prints them reports the defect by committing it. The gate caught its own author before this branch was pushed. The first draft of the workflow comment below said the six characters of a U+061C escape; the tool payload decoded them, a single invisible U+061C landed in drift-guard.yml, and nothing but this scan would have reported it. That is #628's reproduction a fourth time, on the PR that closes it. EXEMPT maps a path to a written reason and is empty, which is the measured state of the tree rather than an aspiration. Both hygiene rules from check_workflow_option_coverage.py's EXEMPT and check_workflow_job_banners.py's UNBANNERED apply and are pinned by the self-test: an entry for a file that does not exist is an error, and an entry for a file with no raw controls is an error. Rides drift-guard.yml's prose-lint job, where #635 put the NOLINT-directive scan for the same reason: a fast, dependency-free text scan that compiles nothing, guarding the neighbouring class of defect -- a file that looks correct to every reader while saying something else. Also closes #642, whose five raw U+200E/U+200F do not exist in the merged tree: `git ls-files` lists 1242 files at c55ea5b and none of them contains any of the twelve codepoints. See the PR body for the measurement. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW
Yaraslaut
added a commit
that referenced
this pull request
Sep 21, 2026
Every line of a new file is a changed line, and the clang-tidy job applies
no path exclusion -- no -exclude in its clang-tidy-diff.py invocation, and
no .clang-tidy anywhere under tests/lint/. The three .cpp fixtures added
for morph#628 therefore produced 14 findings, all inside themselves:
11 clang-diagnostic-unused-const-variable
3 misc-misleading-bidirectional
Reproduced locally on clang-tidy 22.1.8 (CI's pinned major) with the
warning set cmake/compiler_options.cmake builds for Clang -- -Weverything
minus the suppressions in its Clang branch -- and the result matches CI's
deduplicated list file for file, line for line, column for column.
Fixed the way the six fixture directories already under tests/lint/ avoid
this: the fixtures are written so clang-tidy has nothing to say about
them. No new .clang-tidy, no path exclusion, no byte of a raw control
removed from an invalid/ fixture.
- `inline` on each namespace-scope constant. A `constexpr` there has
internal linkage, so an unused one is a diagnostic; an `inline` one
has external linkage and is not. This is house style in any case --
include/morph/ spells 36 of its constants that way -- and it changes
no byte of any string literal.
- Three NOLINTNEXTLINE(misc-misleading-bidirectional), reason above the
directive, never wrapped onto a second line (morph#631/#635).
misc-misleading-bidirectional is not the same gate as this PR's, and the
suppressions are where the two are recorded as disagreeing. It reads the
string's *content*, so it fires on escaped_literals.cpp's `""` and
`""` although those are written as escapes and the file on disk is
pure ASCII; this gate reads the *source bytes*, so it does not. Neither
subsumes the other: clang-tidy sees only C++ translation units the
configure put in the compile database, so it would never have looked at
tst_i18n.qml, a Markdown file or a workflow YAML, while this scan covers
all 1244 tracked text files and cannot see a control that exists only
after escape expansion. Suppressing the check globally, or for
tests/lint/ as a directory, would have thrown away the half of the
coverage this PR does not provide.
Verified, not asserted. Before: 14 findings. After: 0. Mutated back one
change at a time, in place so the .clang-tidy chain resolves identically:
dropping `inline` from escaped_literals.cpp restores 7
unused-const-variable findings, and deleting the three directives
restores all 3 misc-misleading-bidirectional findings -- so both halves
are load-bearing and the probe still measures something. The fixtures
still fail when they should: scripts/test_check_bidi_controls.sh passes
all 15 cases, including "invalid fixture <name> rejected" for each of the
four invalid directories on its own. Also green locally:
check_bidi_controls.py, check_nolint_directives.sh (and again with
tests/lint/bidi_controls named explicitly, since the default target prunes
tests/lint), check_tidy_suppression_scope.sh, check_workflow_job_banners.py,
check_spec_citations.sh, check_ci_clang_pin.sh, check_catch_test_names.sh,
and clang-format --dry-run -Werror over the fixtures (the clang-format job
is whole-tree).
The tracked-text-file count in drift-guard.yml and in the self-test's
header moves 1242 -> 1244: the same measurement, restated on this branch's
new base after morph#647, morph#641 and morph#630 landed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW
Yaraslaut
added a commit
that referenced
this pull request
Sep 21, 2026
…) (#644) * ci: reject raw bidi control characters in first-party files (fixes #628) A bidirectional control renders as nothing and occupies no column, so a diff containing one looks exactly like a diff that does not. In a string literal that is a silently wrong assertion (#610: nine lines of tst_i18n.qml whose expected values held raw U+200E / U+200F / U+061C). In a comment it is source that stops saying what it means (#628 reproduced that on itself, twice, in two files, typing an escape through a JSON-payload tool that decoded it before the file was written). Between them is the trojan-source shape, where an override or isolate makes a line render as a different program from the one that compiles. #610 asked for this lint and deferred it as "a bigger decision". Review is not a control for this class -- the PR that closed #610 was reviewed by people who could not have seen a raw control had one survived -- so a gate is the only thing that can hold the line. scripts/check_bidi_controls.py rejects twelve codepoints in three classes, the list #610 wrote: U+061C, U+200E/U+200F, U+202A-U+202E and U+2066-U+2069. Comments are in scope, not excluded: #628's own reproduction was inside comments, and trojan source lives in comments by construction. The tree is already clean -- 0 raw controls across 1242 tracked files -- so the gate ships green and would be worth nothing unless every way of going blind is closed: - The detector is probed against each declared codepoint, one at a time, before any file is opened. A detect() narrowed to the two marks this repository has actually hit fails on a clean tree. - Scanning zero files is an error, not a pass. - scripts/test_check_bidi_controls.sh holds its own independent list of the twelve and requires each to be found in tests/lint/bidi_controls/. That is what notices a codepoint being deleted from the checker's table, which the checker's in-process probe cannot see, because both sides read it. - The diagnostic replaces each control with a visible <U+200E> marker, and the self-test asserts no diagnostic contains a raw control: a gate against invisible characters that prints them reports the defect by committing it. The gate caught its own author before this branch was pushed. The first draft of the workflow comment below said the six characters of a U+061C escape; the tool payload decoded them, a single invisible U+061C landed in drift-guard.yml, and nothing but this scan would have reported it. That is #628's reproduction a fourth time, on the PR that closes it. EXEMPT maps a path to a written reason and is empty, which is the measured state of the tree rather than an aspiration. Both hygiene rules from check_workflow_option_coverage.py's EXEMPT and check_workflow_job_banners.py's UNBANNERED apply and are pinned by the self-test: an entry for a file that does not exist is an error, and an entry for a file with no raw controls is an error. Rides drift-guard.yml's prose-lint job, where #635 put the NOLINT-directive scan for the same reason: a fast, dependency-free text scan that compiles nothing, guarding the neighbouring class of defect -- a file that looks correct to every reader while saying something else. Also closes #642, whose five raw U+200E/U+200F do not exist in the merged tree: `git ls-files` lists 1242 files at c55ea5b and none of them contains any of the twelve codepoints. See the PR body for the measurement. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW * ci: keep the bidi-control fixtures clean under clang-tidy-diff Every line of a new file is a changed line, and the clang-tidy job applies no path exclusion -- no -exclude in its clang-tidy-diff.py invocation, and no .clang-tidy anywhere under tests/lint/. The three .cpp fixtures added for morph#628 therefore produced 14 findings, all inside themselves: 11 clang-diagnostic-unused-const-variable 3 misc-misleading-bidirectional Reproduced locally on clang-tidy 22.1.8 (CI's pinned major) with the warning set cmake/compiler_options.cmake builds for Clang -- -Weverything minus the suppressions in its Clang branch -- and the result matches CI's deduplicated list file for file, line for line, column for column. Fixed the way the six fixture directories already under tests/lint/ avoid this: the fixtures are written so clang-tidy has nothing to say about them. No new .clang-tidy, no path exclusion, no byte of a raw control removed from an invalid/ fixture. - `inline` on each namespace-scope constant. A `constexpr` there has internal linkage, so an unused one is a diagnostic; an `inline` one has external linkage and is not. This is house style in any case -- include/morph/ spells 36 of its constants that way -- and it changes no byte of any string literal. - Three NOLINTNEXTLINE(misc-misleading-bidirectional), reason above the directive, never wrapped onto a second line (morph#631/#635). misc-misleading-bidirectional is not the same gate as this PR's, and the suppressions are where the two are recorded as disagreeing. It reads the string's *content*, so it fires on escaped_literals.cpp's `""` and `""` although those are written as escapes and the file on disk is pure ASCII; this gate reads the *source bytes*, so it does not. Neither subsumes the other: clang-tidy sees only C++ translation units the configure put in the compile database, so it would never have looked at tst_i18n.qml, a Markdown file or a workflow YAML, while this scan covers all 1244 tracked text files and cannot see a control that exists only after escape expansion. Suppressing the check globally, or for tests/lint/ as a directory, would have thrown away the half of the coverage this PR does not provide. Verified, not asserted. Before: 14 findings. After: 0. Mutated back one change at a time, in place so the .clang-tidy chain resolves identically: dropping `inline` from escaped_literals.cpp restores 7 unused-const-variable findings, and deleting the three directives restores all 3 misc-misleading-bidirectional findings -- so both halves are load-bearing and the probe still measures something. The fixtures still fail when they should: scripts/test_check_bidi_controls.sh passes all 15 cases, including "invalid fixture <name> rejected" for each of the four invalid directories on its own. Also green locally: check_bidi_controls.py, check_nolint_directives.sh (and again with tests/lint/bidi_controls named explicitly, since the default target prunes tests/lint), check_tidy_suppression_scope.sh, check_workflow_job_banners.py, check_spec_citations.sh, check_ci_clang_pin.sh, check_catch_test_names.sh, and clang-format --dry-run -Werror over the fixtures (the clang-format job is whole-tree). The tracked-text-file count in drift-guard.yml and in the self-test's header moves 1242 -> 1244: the same measurement, restated on this branch's new base after morph#647, morph#641 and morph#630 landed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Yaraslaut
added a commit
that referenced
this pull request
Sep 21, 2026
…uding TUs, and drop a deleted workflow from a docstring (fixes #646, fixes #643) (#653) * testkit/qt: clear the 84 clang-tidy findings the two AUTOMOC self-including TUs now report (fixes #646) #647 made the clang-tidy job build `ladder_common_tests_autogen` and `morph_forms_qml_tests_autogen`, so `examples/common/testkit/test_qml_surface.cpp` and `src/qt/forms/tests/tst_main.cpp` parse for the first time and are analysed. Nothing was broken -- clang-tidy-diff reports only changed lines -- but the first PR to touch one of those lines would have inherited findings that were not its own. Re-measured on 0067b5b with the clang-tidy job's own configure flags and clang-tidy 22.1.8 (CI's pinned major), both AUTOMOC targets built first: 81 findings in test_qml_surface.cpp (73 misc-const-correctness, 3 readability-convert-member-functions-to-static, 2 readability-inconsistent-declaration-parameter-name, 2 readability-identifier-length, 1 bugprone-easily-swappable-parameters) and 3 in tst_main.cpp. That reproduces the #647 lane's figure at c55ea5b exactly. 77 of the 84 are fixed rather than suppressed: * 73 `misc-const-correctness` -- local `QTemporaryDir` and fixture-bridge declarations that are never mutated. Applied with clang-tidy --fix, then rewritten to the west-const spelling the rest of the file uses. * 2 `readability-identifier-length` -- `id` -> `rowId`, `ok` -> `okay`. Safe: QmlSurfaceAudit reads `QMetaMethod::name()` and `parameterCount()` and never a parameter name, and the QML fixture text is unchanged. * 1 `misc-use-internal-linkage` -- `MorphFormsQmlTestSetup` moves into an anonymous namespace; QUICK_TEST_MAIN_WITH_SETUP expands in the same TU. * 1 `readability-redundant-access-specifiers` -- the explicitly defaulted default constructor and its `public:` are removed, which also removes the redundancy, since Q_OBJECT ends in `private:`. The remaining 7 get individually reasoned NOLINTNEXTLINEs, reason above the directive (#631/#627's rule) -- no NOLINT sweep and no new `.clang-tidy` entry, which is what #632 was about: * 4 `readability-convert-member-functions-to-static` on Q_PROPERTY readers, a Q_INVOKABLE and a Qt Quick Test setup slot. The reason is shape, not legality: the static form was measured to compile and moc registers the same property, but no bridge these fixtures stand in for has a static property reader, and a Qt slot is a member function by definition. * 2 `readability-inconsistent-declaration-parameter-name` on the two signals. moc's generated definitions name the parameters `_t1`/`_t2`, so no edit to the declarations can remove the mismatch; the finding reaches these files only because the classes are declared in a .cpp. * 1 `bugprone-easily-swappable-parameters` on the file-local `writeQml` helper's two adjacent `const QString&`. Verified, not asserted: * Both files are in this configure's compile_commands.json (703 entries, 695 in-workspace, 270 under examples/ -- above #649's 600/200 floors). * clang-tidy exits 0 on both files afterwards, and with every NOLINTNEXTLINE line stripped it exits 1 reporting exactly the 6 + 1 suppressed findings again. Each directive is load-bearing and the TUs are really analysed, rather than clean because nothing looked at them. * `ladder_common_tests "[qml-surface]"`: 182 assertions in 36 test cases, all passing. * `morph_forms_qml_tests -input src/qt/forms/tests`: 292 passed, 0 failed, the two corpus-reading suites among them -- which is what proves the setup slot still runs after the anonymous-namespace move. * clang-format 22.1.8 clean; check_nolint_directives.sh, check_bidi_controls.py, check_tidy_suppression_scope.sh, check_automoc_includes.sh, check_catch_test_names.sh all pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW * ci: drop the deleted suppression-guard.yml from the banner gate's docstring (fixes #643) #635 deleted .github/workflows/suppression-guard.yml; the checker's "## Scope" paragraph still named it as one of the single-job workflows the gate skips. It was the last reference to that file in the tree: $ git grep -n "suppression-guard" -- . scripts/check_workflow_job_banners.py:50:suppression-guard.yml and the two wasm workflows are single-job files that have Gate behaviour was never affected and is not affected now -- the skip is derived per file, not read from that list. Measured on 0067b5b, over the five workflows with zero banners: ok: .github/workflows/docs.yml: no section banners, not in the banner style ok: .github/workflows/mutation.yml: no section banners, not in the banner style ok: .github/workflows/spec-sync.yml: no section banners, not in the banner style ok: .github/workflows/wasm-demo.yml: no section banners, not in the banner style ok: .github/workflows/wasm-ladder.yml: no section banners, not in the banner style ok: all 24 section banner(s) introduce the job they describe So the name is deleted rather than swapped for another: with it gone the sentence enumerates exactly the five files a run reports as skipped, and there is no sixth current example to put in its place. A second paragraph says so explicitly -- the list is an illustration with a shelf life, nothing reads it, and the run's own output is the current list -- so the next workflow deletion dates one sentence instead of producing a third round of this. Verified: `python3 scripts/check_workflow_job_banners.py .` and `bash scripts/test_check_workflow_job_banners.sh` both pass; the five skipped files and their single-job counts were enumerated from the tree with the checker's own BANNER_RE/JOB_KEY_RE rather than read off the docstring. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two CI-gate tickets on the same tree (
.github/workflows/,scripts/), onecommit each.
#613 — split
mutation_survivors.json's citations by what they arecheck_mutation_survivors.pyaudits only the structured entries and printeda running count of the free-text ones it could not see, several of which were
measurably stale. This splits them by what they actually are rather than
leaving the remainder as an undifferentiated 31:
classes. Theyare converted to the structured
{file, line, source}shape, which theresolver picks up with no change to the script —
find_entries()alreadywalks the whole document, precisely so a new class of live citation is
covered without a second mechanism.
runs,false_positive_finding,mechanism_confirmed,classification_2026_09_09). Their line numbers describe the tree as it waswhen the campaign ran, so each section now carries a
revisionand thecitation resolves with
git show <revision>:<path>. Auditing those againstHEAD would fail forever and correctly so: the record is not wrong, the
reading would be. The module docstring now says this is deliberate, not a
coverage gap awaiting work.
The count is still printed every run, so the gate's coverage stays visible
rather than assumed.
#633 — fold the NOLINT-directive gate into
drift-guard.ymlThe gate from #627 landed as
suppression-guard.ymlonly becausedrift-guard.ymlwas held by an open PR (#614) at the time. That PR haslanded, so the job moves into
prose-lintas two steps andsuppression-guard.ymlis deleted — a fast, dependency-free text scan thatcompiles nothing, riding the same job as the CI-clang-pin and Catch2-name
scans for the same reason, instead of paying for a runner of its own.
The self-test runs first, as every other lint here does: a gate that
detects nothing reports the same green as a clean tree, which for this gate
would be the very defect it exists to catch, one level up.
Verification
Measured in this worktree at
374f389d, rebased ontoa8511aa6:167 directives is the denominator the NOLINT scan was offered, not a hit count
on an empty set. Not verified locally: the full CI matrix — these are the
gate scripts only.
Third commit
374f389drepoints twobackend.hppcitations that the rebase ontoa8511aa6moved. Both entries had recorded in advance that a drift here wouldbe reported as ambiguous rather than auto-corrected, because each cites a
statement appearing twice verbatim; each was resolved by reading the code
(
registerModelnotregisterModelShared; thefetch_addincrement side notthe decrement twin inside the posted task). No
sourceorreasontext wasaltered.
Fixes #613
Fixes #633
🤖 Generated with Claude Code
https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW