From e5e0c880ae69540b4d24409cdc6c462465d6df50 Mon Sep 17 00:00:00 2001 From: Yaraslau Tamashevich Date: Sun, 20 Sep 2026 12:02:19 +0200 Subject: [PATCH 1/3] 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] 92366f61 INFERRED (parent of the commit that recorded it; no citations in it) runs[1] d5c455f2 VERIFIED (both citations resolve) runs[2] adfe8e5f VERIFIED (gh run view 34349442137 headSha; the run id was never a revision, which #613 assumed it was) false_positive_finding d5c455f2 VERIFIED for the four header citations, partial for the tests/ ones mechanism_confirmed 567168b2 PARTIAL, and says so: that campaign measured standalone programs, not this tree classification_2026_09_09 adfe8e5f 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) Claude-Session: https://claude.ai/code/session_01AbwhcguQFkhvVi2AH19sWk --- scripts/check_mutation_survivors.py | 26 +++++-- scripts/mutation_survivors.json | 105 ++++++++++++++++++++++++---- 2 files changed, 110 insertions(+), 21 deletions(-) diff --git a/scripts/check_mutation_survivors.py b/scripts/check_mutation_survivors.py index c19fc4537..2cd4202e7 100644 --- a/scripts/check_mutation_survivors.py +++ b/scripts/check_mutation_survivors.py @@ -46,15 +46,29 @@ ---------------------------------------------------------------- Only the *structured* entries -- dicts carrying `file`, `line` and `source`. The same document also carries citations as free text, inside prose strings: -`"include/morph/core/backend.hpp:613 -- emitMetric(Metric::registerCount, 1.0)"`, -`"site": "remote.hpp:1031"`, and 29 more (31 in all, as this script's own +`"site": "remote.hpp:1031"`, `"core/registry.hpp:54 -- PairKeyHash +hash-combine"`, and 18 more (20 in all, as this script's own count_free_text_citations() reports on every run). Those have no verbatim `source` text to resolve against -- the text after the `--` is a paraphrase, and several use a bare filename with no directory -- so this resolver cannot -see them, and several of them are measurably stale (morph#613). The count is -printed on every run so this gate's coverage of the file is visible rather than -assumed: auditing the structured half of a file while the prose half rots is a -narrower claim than a green tick looks. +see them. + +Every one of those 20 is *deliberately* out of reach, and morph#613 settled +why. They live in the file's dated campaign records -- `runs`, +`false_positive_finding`, `mechanism_confirmed` and +`classification_2026_09_09` -- whose line numbers describe the tree as it was +when the campaign ran, and each of those sections now carries a `revision` so +the citation resolves with `git show :`. Auditing them against +HEAD would fail forever and correctly so: the record is not wrong, the reading +would be. The citations that *do* describe current code all sit under +`classes`, and morph#613 converted them to the structured shape above, which +this script picked up with no edit to it -- find_entries() walks the whole +document by design, exactly so that a new class of live citation is covered +without a second mechanism. + +The count is printed on every run so this gate's coverage of the file is +visible rather than assumed: auditing the structured half of a file while the +prose half is exempt by design is a narrower claim than a green tick looks. Vacuity ------- diff --git a/scripts/mutation_survivors.json b/scripts/mutation_survivors.json index 9d1a6a7cf..0d4336d16 100644 --- a/scripts/mutation_survivors.json +++ b/scripts/mutation_survivors.json @@ -29,15 +29,32 @@ "side channels are recorded separately so a future decision about logs is made about logs.", "But the residual class is an upper bound on missing assertions, not a set of them.", "", - "The `line` on each entry below points at current code, not at where the survivor", - "sat when it was triaged. scripts/check_mutation_survivors.py resolves every entry by", - "its `source` text on every pull request and fails when the hint has drifted (morph#608,", - "which found 5 of 7 already wrong and one off by 479 lines). When it fails, re-read the", - "code at the line it prints, confirm the reason still holds, and update `line`. Deleting", - "an entry, or editing its `source` to match some other line, is a triage nobody did.", - "Note what that gate does NOT cover: the citations written as prose in this file (the", - "`representative_sites` lists, `site` fields, and the narrative in `runs`) carry no", - "verbatim source text, so nothing audits them and several are known stale (morph#613).", + "TWO KINDS OF `line` LIVE IN THIS FILE, AND THEY MEAN OPPOSITE THINGS (morph#613).", + "Which one you are reading is decided by the section, so the split is written down here", + "rather than left to be inferred:", + "", + " LIVE -- everything under `classes`. These describe a classification asserted to hold", + " NOW, so every citation in them points at CURRENT code and is carried as a structured", + " {file, line, source, reason} entry: `classes.equivalent.entries`, the two", + " `representative_sites` lists, and `classes.unasserted.shapes_worth_naming[1].sites`.", + " scripts/check_mutation_survivors.py resolves each of them by its `source` text on", + " every pull request and fails when the hint has drifted (morph#608, which found 5 of 7", + " already wrong and one off by 479 lines). When it fails, re-read the code at the line", + " it prints, confirm the reason still holds, and update `line`. Deleting an entry, or", + " editing its `source` to match some other line, is a triage nobody did. The gate finds", + " these by walking the whole document for {file, line, source} dicts, so a new live", + " citation is audited the moment it is written in that shape -- no script edit needed.", + "", + " HISTORICAL -- `runs`, `false_positive_finding`, `mechanism_confirmed` and", + " `classification_2026_09_09`. These are records of dated campaigns, and their line", + " numbers point at the tree AS IT WAS WHEN THE CAMPAIGN RAN. They are not stale; they", + " are not about HEAD at all. Each such section carries a `revision` (plus a", + " `revision_provenance` saying how well it is established), so a citation resolves with", + " `git show :` and keeps resolving forever. Resolving one against HEAD", + " is the reader error, and the gate deliberately does NOT audit them -- it prints how", + " many it left alone on every run, so a green tick never reads as 'all citations", + " checked'. Do not refresh a historical line number to match today's code: that would", + " falsify the record of what was measured.", "", "Re-measure with: MULL_PREFIX=... bash scripts/mutation.sh core-forms" ], @@ -109,8 +126,18 @@ "meaning": "Removing a log call survives because nothing in morph_tests captures log output.", "reason": "This is not equivalence -- a log line that stops being emitted is an observable change, and morph::log has a sink this suite could install. It is a deliberate absence of assertions on a diagnostic channel, and it is recorded separately from the gaps below so that a future decision to assert on logs (or not to) is made about logs rather than buried in a total.", "representative_sites": [ - "include/morph/core/remote.hpp:1031 -- logError on an undecodable envelope", - "include/morph/core/remote.hpp:1038 -- the one-line-per-request log every kind goes through" + { + "file": "include/morph/core/remote.hpp", + "line": 975, + "source": "::morph::log::logError(", + "reason": "logError on an undecodable envelope -- the decode-failure arm of dispatchMessage. Deleting the call is invisible to morph_tests because no test installs a morph::log sink, so the survivor is a missing assertion on a diagnostic channel, not an equivalent mutant." + }, + { + "file": "include/morph/core/remote.hpp", + "line": 991, + "source": "::morph::log::logDebug(", + "reason": "The one-line-per-request log every kind funnels through, on the successfully-decoded arm. Same disposition as the logError site above: observable in a deployment, unobserved by this suite." + } ] }, "side_channel_metrics": { @@ -118,8 +145,18 @@ "meaning": "Removing an emitMetric/beginSpan call survives.", "reason": "include/morph/core/observability.hpp has a test file of its own (tests/test_observability.cpp), so unlike logging this is a surface the suite already asserts on -- just not on these eight call sites. Closer to a gap than logging is, and separated from it for that reason.", "representative_sites": [ - "include/morph/core/backend.hpp:613 -- emitMetric(Metric::registerCount, 1.0)", - "include/morph/core/backend.hpp:807 -- emitMetric(Metric::executeInFlight, ...)" + { + "file": "include/morph/core/backend.hpp", + "line": 1121, + "source": "::morph::observe::detail::emitMetric(::morph::observe::Metric::registerCount, 1.0);", + "reason": "The registerCount emission on LocalBackend::registerModel. Note for whoever refreshes this hint: the same statement appears character-for-character on the registerModelShared arm as well, so if this `line` ever drifts the gate will report the citation as ambiguous rather than printing a corrected line. That is the right outcome -- which of the two arms is meant is a question for a reader, not for a resolver -- and it is recorded here so the message is not a surprise." + }, + { + "file": "include/morph/core/backend.hpp", + "line": 1285, + "source": "::morph::observe::detail::emitMetric(::morph::observe::Metric::executeInFlight,", + "reason": "The executeInFlight emission on the increment side of an execute. Its decrement twin inside the posted task is the same text after stripping, so the ambiguity note on the registerCount entry above applies here too." + } ] }, "unasserted": { @@ -156,8 +193,34 @@ { "shape": "a boundary comparison is only ever exercised away from its boundary", "count_approx": 30, - "detail": "cxx_gt_to_ge and its family survive on include/morph/core/wire.hpp:514 (the kMaxEnvelopeBytes cap), payload_schema.hpp:169 (kPayloadShapeMaxDepth), forms/instance_constraints.hpp:156 (kMaxDecimalPlaces), forms/forms.hpp:2356 and a dozen more. Each is a comparison tested only at values where moving it by one changes nothing. wire.hpp:514 is the one this run turned into a test -- see tests/test_wire_hardening.cpp, 'decode's size cap is exact at kMaxEnvelopeBytes', whose neighbour was already named 'at the size limit boundary' while asserting a 1 KiB envelope.", - "status": "Real, and three of them are now closed: payload_schema.hpp's kPayloadShapeMaxDepth comparison, instance_constraints.hpp:156's kMaxDecimalPlaces guard and forms.hpp:2356's offender-list separator are all off the survivor list as of runs[1]." + "detail": "cxx_gt_to_ge and its family survive on the four comparisons listed under `sites` below, and on a dozen more. Each is a comparison tested only at values where moving it by one changes nothing. The envelope-size cap is the one this run turned into a test -- see tests/test_wire_hardening.cpp, 'decode's size cap is exact at kMaxEnvelopeBytes', whose neighbour was already named 'at the size limit boundary' while asserting a 1 KiB envelope.", + "sites": [ + { + "file": "include/morph/core/wire.hpp", + "line": 540, + "source": "if (json.size() > kMaxEnvelopeBytes) {", + "reason": "The wire-layer envelope size cap. Turned into an exact boundary assertion by tests/test_wire_hardening.cpp during this run, so it is listed here as the worked example of the shape rather than as an open gap." + }, + { + "file": "include/morph/core/payload_schema.hpp", + "line": 171, + "source": "if (depth > kPayloadShapeMaxDepth) {", + "reason": "The payload-shape depth limit. Closed: off the survivor list as of runs[1], which pinned the comparison at its edge." + }, + { + "file": "include/morph/forms/instance_constraints.hpp", + "line": 156, + "source": "if (places > math::kMaxDecimalPlaces) {", + "reason": "The decimal-places guard. Closed: off the survivor list as of runs[1]." + }, + { + "file": "include/morph/forms/forms.hpp", + "line": 2535, + "source": "if (offenderCount > 0) {", + "reason": "The offender-list separator guard in requiredFieldsNamedBy -- the comparison that decides whether a comma is prepended. Closed: off the survivor list as of runs[1]." + } + ], + "status": "Real, and three of the four sites below are now closed: the payload-shape depth comparison, the decimal-places guard and the offender-list separator are all off the survivor list as of runs[1]." }, { "shape": "a completion is never delivered and nothing waits on it", @@ -170,6 +233,8 @@ "runs": [ { "date": "2026-09-03", + "revision": "92366f61", + "revision_provenance": "INFERRED, not recorded by the run itself. cb6222f6 is the commit that wrote this entry, and 92366f61 is its parent -- the newest tree the baseline could have been measured on. This run carries no line citations, so nothing resolves against the revision; it is here so the entry states which tree it describes rather than only when it was taken.", "scope": "include/morph/core, include/morph/forms", "driven_by": "morph_tests (1279 Catch2 cases, all passing)", "tool": "Mull 0.34.0, mull-runner-22 / mull-ir-frontend-22, LLVM 22.1.2", @@ -192,6 +257,8 @@ { "label": "lane/d-framework, after morph#431 and morph#432's tests", "date": "2026-09-03", + "revision": "d5c455f2", + "revision_provenance": "VERIFIED. d5c455f2 is the commit on lane/d-framework that wrote this entry, and both of the line citations in `movement_against_baseline` below resolve to the text they claim at that revision (checked with `git show`). The branch was merged, not squashed (merge commit 022336e0, morph#436), so the revision is reachable from master and the citations stay resolvable.", "scope": "include/morph/core, include/morph/forms", "driven_by": "morph_tests (1303 Catch2 cases, all passing)", "tool": "Mull 0.34.0, mull-runner-22 / mull-ir-frontend-22, LLVM 22.1.2", @@ -248,6 +315,8 @@ { "label": "first scheduled-gate run, cxx_remove_void_call excluded", "date": "2026-09-09", + "revision": "adfe8e5f317246f6063c4cf3e0a3cf3e23598eda", + "revision_provenance": "VERIFIED. The run id below is a GitHub Actions run, not a commit; `gh run view 34349442137 --json headSha` reports headSha adfe8e5f317246f6063c4cf3e0a3cf3e23598eda on branch master, and `git cat-file -t` confirms that object is a commit in this repository. This is the same revision classification_2026_09_09 classifies, which is why that section's citations resolve against it.", "scope": "core-forms", "driven_by": ".github/workflows/mutation.yml (workflow_dispatch, run 34349442137)", "tool": "Mull 0.34.0 / LLVM 22", @@ -306,6 +375,8 @@ "false_positive_finding": { "verdict": "A survivor in this report is not evidence that the site is unasserted.", "verification_status": "measured, 2026-09-03, on lane/d-framework in the same build tree and configuration the run above used", + "revision": "d5c455f2", + "revision_provenance": "VERIFIED for the header citations. d5c455f2 is the commit on lane/d-framework that wrote this section. At that revision every `site` below resolves to the code it names character-for-character -- the two reply() sites, the emitDeclaredBound() site and the attachIdentity() site -- checked with `git show`. Of the four `tests/` citations, two resolve to exactly the assertion quoted (a REQUIRE on the awaited registration, and the primary-key REQUIRE); the other two resolve to plausible but unquoted assertions and were NOT confirmed against the failure text recorded here.", "method": [ "Four survivors were picked from the run above, from three different headers, and the", "mutation Mull reported was applied to the source by hand -- the call deleted, exactly", @@ -375,6 +446,8 @@ "mechanism_confirmed": { "verdict": "The linker-dedup hypothesis above is REFUTED. The real mechanism is a Mull codegen bug: cxx_remove_void_call's IR pass does not remove the call it claims to remove, for a member-function call site, in Mull 0.34.0.", "verification_status": "measured, 2026-09-04, in an isolated WSL Ubuntu 24.04 environment, Mull-19 0.34.0 / LLVM 19.1.7, clang 19.1.7 -- a different build (Windows) and toolchain from every prior measurement in this file, so the finding is not specific to one environment's linker. Mull 0.34.0 (checked 2026-09-04) is the project's latest release, so there is no newer version to retest the mechanism against.", + "revision": "567168b2", + "revision_provenance": "PARTIAL, and stated as such. 567168b2 is the commit that wrote this section; `git cat-file -t` confirms it. It does NOT pin the measurement, because the measurement was not made against this repository: the four programs under `method` below are standalone .cpp/.hpp files reproduced inline there, built outside the tree. This section carries no line citations into include/morph, so there is nothing here for a revision to resolve. The field is recorded anyway so every dated section in this file states a revision and a reader does not have to work out which ones a revision would help.", "method": [ "The linker-dedup hypothesis predicts the bug disappears once a function has only one copy", "to be discarded -- i.e. a single translation unit, linked alone, with no duplicate object", @@ -438,6 +511,8 @@ ] }, "classification_2026_09_09": { + "revision": "adfe8e5f317246f6063c4cf3e0a3cf3e23598eda", + "revision_provenance": "VERIFIED. This is runs[2]'s revision, already named in prose below as `adfe8e5f`; lifted into a field of its own so it is machine-readable and sits next to the citations it resolves. Every one of this section's line citations was re-resolved at it with `git show` and each names the code it claims -- the Feistel round function, the three hash-combine sites, the fingerprint accumulation, the three permute() lines, and the four `survivor_list_unreliable` samples.", "_comment": [ "Classification of run[2]'s 199 survivors (morph#453 item A).", "Updated 2026-09-10. The headline finding is NOT a classification: sampling", From a9ccd44a91a98af232aa3e5362fdaa8ec0bb58ce Mon Sep 17 00:00:00 2001 From: Yaraslau Tamashevich Date: Sun, 20 Sep 2026 12:03:11 +0200 Subject: [PATCH 2/3] 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) Claude-Session: https://claude.ai/code/session_01AbwhcguQFkhvVi2AH19sWk --- .github/workflows/drift-guard.yml | 49 +++++++++++++++++++- .github/workflows/suppression-guard.yml | 61 ------------------------- 2 files changed, 47 insertions(+), 63 deletions(-) delete mode 100644 .github/workflows/suppression-guard.yml diff --git a/.github/workflows/drift-guard.yml b/.github/workflows/drift-guard.yml index e0c946bca..71ef64059 100644 --- a/.github/workflows/drift-guard.yml +++ b/.github/workflows/drift-guard.yml @@ -3,8 +3,13 @@ name: Drift guard # Gates for the "a list that stopped matching what it describes" class of bug. # Every job here is fast and dependency-free; none of them compiles anything. # -# Sections below: the spec <-> code gates, the ladder rung-list gate, then the -# scenario-coverage gate. +# It also carries one gate for the neighbouring "a suppression that suppresses +# nothing" class (the NOLINT-directive scan at the end of prose-lint, #627), +# which is the same shape of defect one level down: a control that parses, looks +# applied, and measures nothing. +# +# Sections below: the spec <-> code gates and the NOLINT-directive gate, the +# ladder rung-list gate, then the scenario-coverage gate. # # Two independent gates for the "spec <-> code drift" class of bug (a # docs/spec/*.md file stating a mechanical fact -- an enum cardinality, a @@ -157,6 +162,46 @@ jobs: - name: Check every test name selects itself as a Catch2 filter run: bash scripts/check_catch_test_names.sh + # ── NOLINT directives that cannot take effect (#627, folded here by #633) ── + # NOLINTNEXTLINE applies to the next *physical* line. When the reason is + # wrapped onto a second comment line, the directive annotates that + # comment, the findings it names are still reported, and clang-tidy says + # nothing about it -- the directive parses and the file looks annotated. + # Four such directives existed on master and six findings leaked past + # three of them (#627). + # + # That is the failure mode AGENTS.md names first -- a control that + # reports success while measuring nothing -- one level below the gate. It + # also matters for the clang-tidy campaign in #580, whose agreed remedy + # is partly "write an individually reasoned NOLINT": that campaign cannot + # proceed honestly on a tree where NOLINTs silently fail to apply, so + # this gate is its precondition. + # + # A fast, dependency-free text scan over ~700 source files that compiles + # nothing, so it rides this job for the same reason the CI-clang-pin and + # Catch2-name scans above do, rather than paying for a runner of its own. + # It landed as a workflow of its own only because this file was held by + # an open PR at the time (#614); that PR has landed and #633 folds it in. + # + # The scan leaves a deliberate residual: an anchoring `NOLINTNEXTLINE` + # whose own line carries no finding is accepted, because a directive may + # legitimately annotate a line the current check set says nothing about. + # The self-test below pins that, and it is the reason this gate is not a + # blanket "every directive must suppress something". + # + # Self-test first, as every other lint in this repository does: a gate + # that detects nothing reports the same green as a clean tree, and for + # this gate that would be the very defect it exists to catch, one level + # up. It drives the checker against tests/lint/nolint_directives/ -- the + # two effective shapes must be accepted, each inert shape rejected on its + # own and naming its own file, and a directory with no directives at all + # rejected rather than called clean. + - name: Self-test the NOLINT-directive checker + run: bash scripts/test_check_nolint_directives.sh + + - name: Check every NOLINTNEXTLINE annotates a line of code + run: bash scripts/check_nolint_directives.sh + # ── Ladder rung list <-> the CI filters that are supposed to track it ── # examples/rungs.txt is the ladder's single authoritative rung list, and # almost every consumer now derives from it at run time. Three cannot -- diff --git a/.github/workflows/suppression-guard.yml b/.github/workflows/suppression-guard.yml deleted file mode 100644 index 5817b9abf..000000000 --- a/.github/workflows/suppression-guard.yml +++ /dev/null @@ -1,61 +0,0 @@ -name: Suppression guard - -# Gates for the "a suppression that suppresses nothing" class of bug. -# -# NOLINTNEXTLINE applies to the next *physical* line. When the reason is wrapped -# onto a second comment line, the directive annotates that comment, the findings -# it names are still reported, and clang-tidy says nothing about it -- the -# directive parses and the file looks annotated. Four such directives existed on -# master and six findings leaked past three of them (#627). -# -# That is the failure mode AGENTS.md names first -- a control that reports -# success while measuring nothing -- one level below the gate. It also matters -# for the clang-tidy campaign in #580, whose agreed remedy is partly "write an -# individually reasoned NOLINT": that campaign cannot proceed honestly on a tree -# where NOLINTs silently fail to apply, so this gate is its precondition. -# -# WHY THIS IS ITS OWN WORKFLOW, AND WHERE IT BELONGS -# -# This job is a fast, dependency-free text scan that compiles nothing, which is -# exactly .github/workflows/drift-guard.yml's remit, and it should be a job in -# that file. It is here instead only because drift-guard.yml was held by an open -# PR (#614) when this landed, and ci.yml by another (#623). Folding it into -# drift-guard.yml once that has landed is tracked as its own issue; nothing -# about the gate's behaviour changes when it moves. -# -# The self-test runs before the gate, as every other lint in this repository -# does: a gate that detects nothing reports the same green as a clean tree, and -# for this gate that would be the very defect it exists to catch, one level up. - -on: - push: - branches: [main, master] - pull_request: - -# Supersede a run a newer commit on the same ref has made obsolete -- see the -# note in ci.yml. Cheap here, but the queue it shares is account-wide. -concurrency: - group: suppression-guard-${{ github.ref }} - cancel-in-progress: true - -permissions: - contents: read - -jobs: - nolint-lint: - name: NOLINT directives that cannot take effect - runs-on: ubuntu-24.04 - steps: - - name: Checkout - uses: actions/checkout@v4 - - # Drives the checker against tests/lint/nolint_directives/ first: the two - # effective shapes must be accepted, each inert shape must be rejected on - # its own and name its own file, and a directory with no directives at all - # must be rejected rather than called clean. Without this the step below - # would report green whether or not it still detects anything. - - name: Self-test the NOLINT-directive checker - run: bash scripts/test_check_nolint_directives.sh - - - name: Check every NOLINTNEXTLINE annotates a line of code - run: bash scripts/check_nolint_directives.sh From 374f389d8f413d8fcc517d06933a01c08e5f9649 Mon Sep 17 00:00:00 2001 From: Yaraslau Tamashevich Date: Sun, 20 Sep 2026 21:22:15 +0200 Subject: [PATCH 3/3] ci: repoint two backend.hpp citations this rebase moved Rebasing this branch onto a8511aa6 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) Claude-Session: https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW --- scripts/mutation_survivors.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/mutation_survivors.json b/scripts/mutation_survivors.json index 0d4336d16..9a54616b0 100644 --- a/scripts/mutation_survivors.json +++ b/scripts/mutation_survivors.json @@ -147,13 +147,13 @@ "representative_sites": [ { "file": "include/morph/core/backend.hpp", - "line": 1121, + "line": 1215, "source": "::morph::observe::detail::emitMetric(::morph::observe::Metric::registerCount, 1.0);", "reason": "The registerCount emission on LocalBackend::registerModel. Note for whoever refreshes this hint: the same statement appears character-for-character on the registerModelShared arm as well, so if this `line` ever drifts the gate will report the citation as ambiguous rather than printing a corrected line. That is the right outcome -- which of the two arms is meant is a question for a reader, not for a resolver -- and it is recorded here so the message is not a surprise." }, { "file": "include/morph/core/backend.hpp", - "line": 1285, + "line": 1379, "source": "::morph::observe::detail::emitMetric(::morph::observe::Metric::executeInFlight,", "reason": "The executeInFlight emission on the increment side of an execute. Its decrement twin inside the posted task is the same text after stripping, so the ambiguity note on the registerCount entry above applies here too." }