Skip to content

ci: audit mutation_survivors.json's line citations instead of hoping (fixes #608) - #614

Merged
Yaraslaut merged 1 commit into
masterfrom
lane/b-mutation-survivor-citations
Sep 20, 2026
Merged

Yaraslaut merged 1 commit into
masterfrom
lane/b-mutation-survivor-citations

Conversation

@Yaraslaut

Copy link
Copy Markdown
Member

fixes #608

What this is

scripts/mutation_survivors.json cites each triaged equivalent mutant by
{file, line, mutator, source, reason}. Nothing read the line field, and 5 of
the 7 entries pointed at the wrong line. Two halves, and the second is the
load-bearing one:

  1. The five stale hints are refreshedbridge.hpp 119→121,
    backend.hpp 749→1228, wire.hpp 302→307, views.hpp 264→271 and
    430→437.
  2. A new gate makes them audited, so they cannot rot silently again:
    scripts/check_mutation_survivors.py.

The premise, re-measured on a020e69c rather than copied from the issue

entries with file/line/source: 7
include/morph/core/bridge.hpp:119 -- text now at line(s) [121] | source='return modelHash ^ (key.sharing.hash_code() + 0x9e3779b9U + (modelHash << 6) + (modelHash >> 2));'
include/morph/core/backend.hpp:749 -- text now at line(s) [1228] | source='aware.reserve(_changeAware.size());'
include/morph/core/wire.hpp:302 -- text now at line(s) [307] | source='out.reserve(text.size());'
include/morph/forms/views.hpp:264 -- text now at line(s) [271] | source='ordered.reserve(propsObj.size());'
include/morph/forms/views.hpp:430 -- text now at line(s) [437] | source='ids.reserve(_providers.size());'
stale: 5 of 7

Still 5 of 7, and backend.hpp has drifted further since the issue was written
(it reported 1226; 1228 now, i.e. 479 lines off, landing in the middle of an
unrelated function). Every source text still exists and is still unique, so no
disposition was wrong — only the coordinates.

Reused, not reinvented

The issue named the precedent precisely, and it holds up: the gate calls
check_branch_coverage.py's resolve_allowlist_source_line() directly. That is
the same function hardening scripts/branch_partial_allowlist.json and
scripts/error_path_allowlist.json, and its own docstring says it was factored
out to be shared (check_error_path_coverage.py is caller two, morph#406). This
is caller three. Reusing it rather than mirroring it was the right call: the
"moved line / vanished text / ambiguous match" resolution is the fix for a
defect this repository has found three times (morph#349, morph#355, morph#419),
and a second independently maintained copy of that fix is how the class gets a
fourth chance. Nothing in it needed changing.

Verification — mutation, not assertion

1. The gate reproduces the defect on the real file

Run against the un-refreshed mutation_survivors.json (i.e. master's
data), before this PR's refresh:

scripts/mutation_survivors.json: 5 citation(s) no longer match the code they name.

  - include/morph/core/bridge.hpp:119 has moved to line 121. The text still matches, so nothing is wrong with the disposition -- update the `line` hint.
  - include/morph/core/backend.hpp:749 has moved to line 1228. The text still matches, so nothing is wrong with the disposition -- update the `line` hint.
  - include/morph/core/wire.hpp:302 has moved to line 307. The text still matches, so nothing is wrong with the disposition -- update the `line` hint.
  - include/morph/forms/views.hpp:264 has moved to line 271. The text still matches, so nothing is wrong with the disposition -- update the `line` hint.
  - include/morph/forms/views.hpp:430 has moved to line 437. The text still matches, so nothing is wrong with the disposition -- update the `line` hint.

These are triage dispositions, not suppressions: the reasoning is recorded
because someone checked it. Re-read the code at the resolved line, confirm the
reason still holds, and then update the entry's `line` to the number printed
above. Do not delete an entry to silence this gate, and do not move a `source`
text to make it match -- either is a triage nobody performed.
GATE RC=1

That is the issue's own measurement, produced by the gate, with no extra tooling.

2. Deliberately corrupting one refreshed hint fails the gate

sed -i 's/"line": 307,/"line": 308,/' scripts/mutation_survivors.json, then
python3 scripts/check_mutation_survivors.py:

scripts/mutation_survivors.json: 1 citation(s) no longer match the code they name.

  - include/morph/core/wire.hpp:308 has moved to line 307. The text still matches, so nothing is wrong with the disposition -- update the `line` hint.

These are triage dispositions, not suppressions: the reasoning is recorded
because someone checked it. Re-read the code at the resolved line, confirm the
reason still holds, and then update the entry's `line` to the number printed
above. Do not delete an entry to silence this gate, and do not move a `source`
text to make it match -- either is a triage nobody performed.
RC=1

Hint restored; gate green again.

3. Mutating the gate turns its self-test red

Replacing the resolver call with resolved = hint # MUTANT: skip the resolver entirely — the exact "check that measures nothing" this PR is about — leaves
the gate green on the real file, which is precisely why the self-test has to
be the thing that catches it. --self-test under that mutant:

error: a drifted `line` hint passed the gate
error: a citation whose source text no longer exists passed the gate
error: an ambiguous citation passed the gate
error: an entry outside classes.equivalent was not audited
error: a citation naming a deleted file passed the gate

5 self-test check(s) failed

(The remaining 5 cases — the counted-citation report, the source-less entry, the
reason-less entry, the empty document, the free-text counter — legitimately do
not depend on the resolver and still pass, which is what you want a mutation
result to look like.)

4. Clean on the refreshed file

ok: 7 structured citation(s) in scripts/mutation_survivors.json resolve to the line they name.
note: 31 further citation(s) in this file are free text inside prose
      strings, which carry no verbatim `source` and so are NOT audited here (morph#613).

all self-test checks passed

Also re-run green and unaffected: check_branch_coverage.py --self-test,
check_error_path_coverage.py --self-test,
check_mutation_regression.py --self-test, scripts/check_spec_citations.sh.

Which CI job runs it

.github/workflows/drift-guard.yml, job mutation-regression-lint (display
name updated to "Mutation gates (regression self-test, survivor citations)",
since it is no longer only a self-test). Two steps:

  • Audit the mutation-survivor citationspython3 scripts/check_mutation_survivors.py
  • Self-test the mutation-survivor citation gatepython3 scripts/check_mutation_survivors.py --self-test

Unlike the other allowlist gates, this one needs no build and no Mull report
— its inputs are the JSON and the headers — so it runs as the gate itself,
per-PR, in seconds. That matters: the campaign is scheduled, but what the
citations rot against is ordinary per-PR edits to include/morph. (Master has
no branch protection, so renaming the job's display name breaks no required
check; verified via gh api .../branches/master/protection → 404 "Branch not
protected".)

Anti-vacuity, beyond the mutation test

Two things are refused rather than skipped:

  • No auditable entries at all fails. Entries are found by walking the whole
    document for dicts carrying file/line/source — so a class a future
    triage pass adds is audited without editing the script — but a restructure
    that moved them all would otherwise leave the gate green over zero entries.
  • An entry with file/line but no source fails rather than being
    skipped as "not mine". Silently ignoring it is how the next contributor adds
    exactly the unauditable citation this gate exists to forbid.

The failure message tells a reader how to refresh, never how to suppress:
re-read the code at the printed line, confirm the reason still holds, update
line — and explicitly, do not delete the entry and do not edit source to
match some other line.

The two things the issue left unverified — both answered

1. Were the two non-stale entries non-stale by luck, or refreshed? By
luck.
Measured by replaying every revision of the file and resolving each entry
against the source at that revision:

cb6222f6 mutation: measure whether the suite would notice include/mor
     registry.hpp:54 ok / bridge.hpp:119 ok / backend.hpp:749 ok / logger.hpp:78 ok / wire.hpp:302 ok / views.hpp:264 ok / views.hpp:430 ok
567168b2 mutation: confirm the false-positive mechanism -- cxx_remove
     backend.hpp:749 STALE(actual [738])
136146c1 Batch: thirteen audit findings -- data loss, a security gap,
     bridge.hpp:119 STALE(actual [121]) / backend.hpp:749 STALE(actual [774]) / wire.hpp:302 STALE(actual [307]) / views.hpp:264 STALE(actual [271]) / views.hpp:430 STALE(actual [437])
c1ffb620 Close #506 with a reproduction, and establish that the mutat
     bridge.hpp:119 STALE(actual [121]) / backend.hpp:749 STALE(actual [776]) / wire.hpp:302 STALE(actual [307]) / views.hpp:264 STALE(actual [271]) / views.hpp:430 STALE(actual [437])

The seven line values are byte-identical in every revision from the file's
creation to a020e69cno line hint in this file has ever been edited.
registry.hpp:54 and logger.hpp:78 are still right only because nothing above
them moved. (Note also backend.hpp's drift is not monotonic — 738, then 774,
then 776, now 1228 — so "just add the delta" was never available either.)

2. The plain-string citations in the runs narrative. Measured, and they
are stale.
backend.hpp:613 is now /// and :807 a doc comment; the string
emitMetric(Metric::registerCount does not occur anywhere in backend.hpp any
more (every call site is written fully qualified, at lines 1121/1140/1285). At
least 10 of the 31 free-text citations are stale, 6 are still correct, and 1
(payload_schema.hpp:169) names a directory that does not exist.

This gate cannot see any of them, and that is worth stating plainly: they
carry no verbatim source to resolve against — the text after the -- is a
paraphrase and several use a bare filename. Auditing the structured half while
the prose half rots is a narrower claim than a green tick looks, so the gate
prints its own narrowness on every run (note: 31 further citation(s) ... are NOT audited here). Filed as #613 rather than folded in: different mechanism,
and AGENTS.md is explicit that sharing a theme is not grounds for consolidation.

One nice detail out of that measurement: the prose at classification_2026_09_09
cited core/bridge.hpp:121 for the HandlerKey hash-combine — correct — while
the structured entry for the same site said 119. They now agree.

Contention warning for whoever lands this

The refreshed hints point into include/morph/core/bridge.hpp and
backend.hpp, which a concurrent lane is actively rewriting. 1228 and 121 may
already be stale by the time that lane lands
, and if so this new gate will
fail on their PR. That is the gate working, not a defect — and it is the
strongest argument for the resolver-based approach over hand-maintained
coordinates: the failure message prints the corrected line, so the fix is
mechanical (edit two numbers) instead of an investigation. Editing this JSON
does not touch those headers, so there is no merge conflict, only a possible
post-merge refresh.

Not folded in

Issues filed

CI status at hand-off

Incomplete. This PR is opened and handed back immediately without waiting
for CI; no check results had arrived at push time. Local runs only, as quoted
above.

🤖 Generated with Claude Code

https://claude.ai/code/session_01AbwhcguQFkhvVi2AH19sWk

…ixes #608)

scripts/mutation_survivors.json cites each triaged equivalent mutant by
{file, line, mutator, source, reason}. Nothing read the `line` field, and
5 of the 7 entries pointed at the wrong line -- core/backend.hpp's by 479,
landing in the middle of an unrelated function, which is where
check_mutation_regression.py's own failure message sends a human who finds
a new survivor. Re-checking the file's history shows no `line` in it has
ever been edited since it was written, so the two entries that were still
right were right by luck, not by refresh.

Two halves, and the second is the one that matters:

  * Refresh the five hints (bridge 119->121, backend 749->1228, wire
    302->307, views 264->271 and 430->437). Every `source` text still
    exists and is still unique, so no disposition was wrong -- only the
    coordinates. Four `reason` strings cross-referenced "backend.hpp:749"
    in prose; they now name the entry's source line instead of a number
    that rots.

  * scripts/check_mutation_survivors.py makes the hints audited, so they
    cannot rot silently again. It calls check_branch_coverage.py's
    resolve_allowlist_source_line() rather than reimplementing it -- the
    same function that hardens branch_partial_allowlist.json and
    error_path_allowlist.json against this defect, found three times over
    (morph#349, morph#355, morph#419). A second, independently maintained
    copy of that fix is how the class gets a fourth chance.

The gate needs no build and no Mull report -- its inputs are the JSON and
the headers -- so it runs as the gate itself, per-PR, in drift-guard.yml's
mutation job, not only as a self-test. What it audits rots per-PR, which is
where it belongs; the campaign itself stays on its schedule.

Two things are refused rather than skipped, because a check that reports
success while measuring nothing is the failure this repository keeps
hitting: finding no auditable entries at all, and an entry carrying
`file`/`line` but no `source`. Entries are located by walking the whole
document, so a class added by a future triage pass is audited without
editing the script.

Verified by mutation, not by assertion. Deliberately drifting one refreshed
hint (wire.hpp 307 -> 308) fails the gate and prints the corrected line;
removing the resolver call from the script leaves the gate green on the
real file but turns 5 of its 10 self-test cases red. The gate also
reproduces morph#608's measurement exactly when run against the
un-refreshed file: the same 5 entries, the same corrected lines.

What this does not cover, and says so on every run: the same file's 31
free-text citations, inside prose strings, which carry no verbatim source
to resolve against. At least 10 of them are stale; filed as morph#613
rather than folded in, since they need a different mechanism.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AbwhcguQFkhvVi2AH19sWk
@Yaraslaut

Copy link
Copy Markdown
Member Author

Merge ordering, decided by the runner: #585 first, then #614

These two PRs interact, and the interaction is measured rather than predicted. #614 adds a per-PR gate that resolves scripts/mutation_survivors.json's {file, line, source} citations against the tree and fails when a line hint has drifted. #585 moves code in both files two of those citations point into.

Measured against #585's head 49bd5b8b and #614's refreshed JSON:

include/morph/core/bridge.hpp
   #614 refreshed it to 121; on #585's branch the text is at [122]   -> WILL GO STALE
include/morph/core/backend.hpp
   #614 refreshed it to 1228; on #585's branch the text is at [1322] -> WILL GO STALE

The backend.hpp delta is 94 lines, exactly the +94 #585 adds to that file. This is not a merge conflict — #585 does not touch the JSON, and #614 does not touch the headers — so git will merge them cleanly and the gate is what will object.

Why this order:

So: land #585 when green. Then rebase #614, re-run its gate (python3 scripts/check_mutation_survivors.py), update the two hints it names, and land it.

This is the gate working as designed, and it is the argument #614's PR body makes for the resolver approach over hand-maintained coordinates — recorded here so whoever runs the next landing sweep does not have to re-derive it.

@codecov

codecov Bot commented Sep 20, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@Yaraslaut

Copy link
Copy Markdown
Member Author

@Yaraslaut
Yaraslaut merged commit 553267a into master Sep 20, 2026
49 checks passed
Yaraslaut added a commit that referenced this pull request Sep 20, 2026
…rt every surface it measured (fixes #605, fixes #618) (#623)

* ci: build MORPH_BUILD_BANK_GUI, and derive the option list rather than assert it (fixes #605)

`MORPH_BUILD_BANK_GUI` defaults OFF and was set ON only by the
`linux-everything` preset in CMakePresets.json, which no workflow names. The
native `bank_gui` / `bank_gui_qml_tests` targets were therefore compiled by no
job, which is how #604 happened: a target went unbuildable on master with every
gate green, and the job called "Linux / all optional features" went on passing,
because its option list is hand-written and nothing compared it to the options
that exist.

Two parts.

(a) Add `-DMORPH_BUILD_BANK_GUI=ON` and `-DMORPH_BUILD_BANK_EXAMPLE=ON` (which
it needs -- the root CMakeLists only descends into examples/bank under the
latter, and examples/bank only into gui/ under the former) to the
`linux-all-features` configure step. That is the one leg that can host them: it
already installs Qt 6.8.1 from aqtinstall, which the GUI's
Qml/Quick/QuickControls2 at 6.5+ requires and the distro Qt the other bank jobs
use cannot give, and already installs the ODBC/SQLite/yaml-cpp/libzip set the
bank example's Lightweight fetch needs.

(b) A gate that derives the list. Adding the flag closes the hole once; it does
not close the class, and AGENTS.md's own test -- "ask whether the check would
still pass if the feature did nothing" -- answers yes for (a) alone. Nothing
stops option 17 from being declared next month and enabled by nobody.
scripts/check_workflow_option_coverage.py enumerates every
`option(MORPH_BUILD_...)` in the tracked CMake files and every
`-DMORPH_BUILD_*=ON` in .github/workflows/, and fails when a declared option is
built by nothing. Covered means: declared default ON, or enabled by a native
workflow, or enabled through a matrix key some leg sets ON, or exempt with a
written reason. Its failure message says how to cover the option, not how to
exempt it.

Two corrections to the issue's accounting, both found by writing the check:

- `MORPH_BUILD_DOCUMENTATION` is *not* exempt. The issue put it in the
  exemption set beside `CLANG_TIDY`, but docs.yml's configure step passes
  `-DMORPH_BUILD_DOCUMENTATION=ON` literally, so it is covered by derivation.
  Exempting it would have been exactly the hand-maintained falsehood this gate
  exists to prevent, so the checker also rejects an exemption for an option
  that is in fact covered, and one naming an option no CMake file declares.
  `MORPH_BUILD_CLANG_TIDY` is the only exemption.

- `MORPH_BUILD_BANK_GUI` was not enabled by *nothing*: wasm-demo.yml has passed
  `-DMORPH_BUILD_BANK_GUI=ON` all along. Under EMSCRIPTEN,
  examples/bank/CMakeLists.txt descends into gui_wasm/ and returns before the
  native gui/ exists, so the same option name selects a disjoint subtree and
  that build proves nothing about the target #604 found broken. Counting it
  would have made this gate vacuous for the one option it was written for, so
  Emscripten workflows are classified apart -- derived from the workflow text
  (`emcmake`/`emsdk`/`qt-cmake`), not from a list of filenames.

The checker is not trusted on its own report: scripts/test_check_workflow_-
option_coverage.sh reintroduces each drift it claims to catch into a scratch
copy of the tree, one at a time, and requires it to be caught for the stated
reason -- including dropping (a)'s own flag, which must go red despite
wasm-demo.yml. Twelve cases, nine of them mutations and three false-positive
mirrors. The `option-coverage` job runs the self-test before the check, the
same order the deprecation, test-type-name and journal-stamp lints use.

Its home is drift-guard.yml once #614 lands; that file is held, so it is here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AbwhcguQFkhvVi2AH19sWk

* ci: a red leg reports every test surface it could measure (fixes #618)

Three jobs run disjoint test activities in sequence with no condition on the
later steps, so the implicit `success()` means a failure in the first stops the
second from running at all. The surface the second covers is then measured
nowhere, and nothing in the log distinguishes "fine" from "never attempted" --
discovering it costs a full 26-74 minute cycle on the next push, including
every green leg that had nothing to do with it.

`!cancelled()` on the later step in each:

- ladder-tests: "Run the scenario corpus against the built servers", a
  different surface from the ctest step above it.
- ladder-sanitizers: "Test (the Qt transport suites, which no sanitizer leg
  ran)". This is the case that settles the argument -- by that step's own
  comment it is the only leg in CI that runs the Qt transport suites under any
  sanitizer.
- linux-all-features: "Verify the fuzz replay covered the committed
  reproducers", which checks whether a guard went vacuous and cannot be
  inferred from the ctest step's result.

`!cancelled()`, not `always()`. `always()` also fires during teardown of a
cancelled run, which makes a leg that stopped within seconds of its siblings
read as a defect rather than a cancellation -- the artefact this repository's
own triage guidance warns about. `!cancelled()` runs on success or failure and
stops on cancellation. Each condition keeps the guard that was already there
(`steps.filter.outputs.run`, `matrix.fuzzers`), so nothing newly runs on a leg
that was being skipped for its own reasons.

Neither condition changes a job's conclusion: a red step keeps the job red.
This makes a failing leg report more, not gate less.

linux-coverage is deliberately not changed, and now says so in a comment next
to the steps in question. Its "Generate coverage report" and the two steps
after it consume the Test step's `.profraw` output rather than measuring a
surface of their own; forcing them to run over truncated profile data would
produce a coverage number computed from a partial pass, and a number nobody
should trust is worse than no number. Written down because it is the part
someone would otherwise "fix" later.

Verification status: INFERRED, NOT REPRODUCED. The acceptance the issue asks
for -- deliberately fail the first test step of one of these jobs and observe
the later one still run and still report, on a red job -- requires a CI cycle
against a deliberately broken branch, which this lane cannot run. What was
measured: the edited ci.yml parses, and each of the three steps carries exactly
`!cancelled() && <its pre-existing guard>` while linux-coverage carries none.
The behavioural claim rests on GitHub Actions' documented status-check-function
semantics, the same standard as the issue's and the triage's own premise check.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AbwhcguQFkhvVi2AH19sWk

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Yaraslaut added a commit that referenced this pull request Sep 20, 2026
…, and clear clang-tidy batch A (fixes #627, fixes #600) (#631)

* lint: make four NOLINTNEXTLINE directives apply, and gate the ones that cannot (fixes #627)

A NOLINTNEXTLINE annotates the next *physical* line. Four directives in the
tree had their reason wrapped onto a second comment line, so each annotated
that comment instead of the code, and clang-tidy reported nothing about it --
the directive parses, the file looks annotated, and the findings leak.

Measured on 0e3b882, clang-tidy 22.1.8, with the CI clang-tidy job's own
option set. Before, the four leaked six findings:

  forms.hpp:489:39  forwarding reference parameter 'action' is never forwarded
  forms.hpp:489:57  forwarding reference parameter 'visitor' is never forwarded
  forms.hpp:496:41  possibly unsafe 'operator[]'
  forms.hpp:496:66  possibly unsafe 'operator[]'
  oom_injector.cpp:110:21  do not manage memory manually
  oom_injector.cpp:110:9   initializing non-owner with a newly created owner

After, all six are gone. The fourth directive, test_bridge_lifetime.cpp:519,
was inert *and* unnecessary -- cppcoreguidelines-owning-memory does not fire on
placement new, confirmed by measurement -- so it is now merely effective, and
kept so a later edit to that line cannot reintroduce the finding silently.

Each site moves its reason above the directive rather than adding a
clang-format guard, so the fix survives reformatting. forms.hpp:487's directive
also dropped an unchecked-container-access it never needed; only
missing-std-forward fires on that line, and the reason now says why neither
parameter may be forwarded rather than restating the check's name.

The guard is the part that matters. scripts/check_nolint_directives.sh fails
when a NOLINTNEXTLINE is followed by a comment, a blank line, or nothing at
all. Run against unmodified master it reports exactly the four sites above, at
exactly those line numbers, and it does not flag fixed_string.hpp:48 -- the
prose that documents this hazard and whose existence is why the scan anchors
the directive at the start of the comment. That anchoring is a stated residual,
not an oversight.

scripts/test_check_nolint_directives.sh drives the checker against
tests/lint/nolint_directives/: the two effective shapes must be accepted, each
inert shape rejected on its own while naming its own file, and a directory with
no directives at all rejected rather than called clean. A gate for suppressions
that suppress nothing would be the same defect one level up if it were not
itself tested.

The job belongs in drift-guard.yml and is in its own workflow only because that
file and ci.yml are both held by open PRs (#614, #623); the workflow's header
says so and folding it in changes nothing about its behaviour.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AbwhcguQFkhvVi2AH19sWk

* lint: clear clang-tidy batch A, and set the campaign's suppression precedents (fixes #600)

#580's census of 596 findings splits into six batches by file tree. Batch A is
the tail -- version/attributes/journal/render/qt-forms/detail -- and it is first
not because it is smallest but because it is the only batch that forces every
policy precedent the other five need, on a corpus where getting one wrong is
cheap. `include/morph/render/locale_format.hpp` is excluded: PR #630 rewrites it
and changes its answer, so its one finding must be re-measured, not inherited.

Measured on 0e3b882, clang-tidy 22.1.8, .clang-tidy unmodified, with the CI
clang-tidy job's own configure. Nine TUs -- the VIHS stub for each batch-A
header plus quantity.hpp's stub, examples/forms/gui_qml/FormsController.cpp and
tests/test_quantity.cpp for the findings a standalone header stub cannot reach
because it instantiates nothing. Before, deduplicated by path+line+column, that
reproduces the census row for row:

  7  include/morph/version.hpp
  4  include/morph/journal/action_log.hpp
  4  include/morph/qt/forms/forms_controller_core.hpp
  2  include/morph/journal/file_action_log.hpp
  1  include/morph/attributes.hpp
  1  include/morph/detail/quantity_equation.hpp
  1  include/morph/render/i18n.hpp
  --
  20

After: 0. The same run still reports 261 findings elsewhere under
include/morph/, so it analysed the tree rather than failing to.

Fixed rather than suppressed, four checks:

  readability-use-concise-preprocessor-directives (1) -- attributes.hpp's
  `#if defined(__has_cpp_attribute)` is now `#ifdef`. This is #600's finding.

  readability-redundant-member-init (4) -- action_log.hpp's `std::string x{}`
  members drop the initializer. std::string's default constructor is
  non-trivial, so aggregate and default initialization are unchanged.

  readability-identifier-length (1) -- file_action_log.hpp's `std::ifstream in`
  becomes `input`.

  performance-unnecessary-value-param (4) -- forms_controller_core.hpp's
  submitIfValid/fetchOptions took `std::string` by value and passed it to
  executeJson, whose parameters are `std::string_view`. Neither was ever moved,
  so the copies bought nothing; both are now `const std::string&`, which is
  source-compatible. examples/bookmarks' mirror moves with it -- its own doc
  comment asserts it has the same body as this one, and that claim has to stay
  true.

  cppcoreguidelines-pro-bounds-avoid-unchecked-container-access (1) --
  file_action_log.hpp's `lines[i]` becomes `lines.at(i)`, hoisted out of the
  try/catch that surrounds it. The loop condition already bounds `i`, so the
  check cannot fire; if it ever could, std::out_of_range inside that try would
  be caught and mis-reported as a malformed journal line.

Suppressed with a reason, three checks -- the precedents the remaining batches
inherit, written to the standard set by render/locale_format.hpp:181:

  macro-to-enum + macro-usage (7) -- version.hpp's macros are the `#if`-testable
  half of the version API. An enumerator is invisible to the preprocessor and a
  constexpr function cannot be called from a `#if`, so the checks do not propose
  a different spelling, they propose removing the capability. Unfixable by
  construction; the constants the checks ask for already exist beside them,
  defined from the macros so the two cannot drift.

  bugprone-easily-swappable-parameters (1) -- render/i18n.hpp's resolveText.
  The check is right that swapping derivedKey and schemaLiteral would be silent.
  They stay because the order is the documented resolution chain, mirrored
  parameter-for-parameter by DynamicForm.qml's resolveText.

  misc-header-include-cycle (1) -- quantity_equation.hpp's include back into
  quantity.hpp. The cycle is real as a graph statement and deliberate as a
  design: it is closed by `#pragma once` and it is what makes the header
  analysable standalone. The remedy the check proposes is the state this file
  was moved away from.

No bare NOLINT anywhere: each suppression says why the check is wrong at that
site, not what the check is called.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AbwhcguQFkhvVi2AH19sWk

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Yaraslaut added a commit that referenced this pull request Sep 20, 2026
`scripts/check_mutation_survivors.py`, which landed on master in #614 after
this branch was cut, resolves each triaged equivalent mutant's `source`
text and fails when the `line` hint has drifted. Two entries name code
this branch moves:

    include/morph/core/bridge.hpp:121  has moved to line 122
    include/morph/core/backend.hpp:1228 has moved to line 1322

Both `source` texts still match uniquely, so neither disposition changed --
only the coordinates. `backend.hpp`'s 94-line delta is this branch's; the
`bridge.hpp` one-line shift likewise.

This is the gate working as designed, and was predicted before either
branch was written: #614's PR body records that these two hints would go
stale when this branch landed, and named the two files.

Verified: `python3 scripts/check_mutation_survivors.py` reports
`ok: 7 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_01AbwhcguQFkhvVi2AH19sWk
Yaraslaut added a commit that referenced this pull request Sep 20, 2026
…old the NOLINT gate into drift-guard (fixes #613, fixes #633) (#635)

* ci: split mutation_survivors.json's citations by what they are (fixes #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

* ci: fold the NOLINT-directive gate into drift-guard.yml (fixes #633)

#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

* ci: repoint two backend.hpp citations this rebase moved

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant