ci: audit mutation_survivors.json's line citations instead of hoping (fixes #608) - #614
Conversation
…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
Merge ordering, decided by the runner: #585 first, then #614These two PRs interact, and the interaction is measured rather than predicted. #614 adds a per-PR gate that resolves Measured against #585's head The Why this order:
So: land #585 when green. Then rebase #614, re-run its gate ( 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 Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
|
|
…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>
…, 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>
`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
…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>
fixes #608What this is
scripts/mutation_survivors.jsoncites each triaged equivalent mutant by{file, line, mutator, source, reason}. Nothing read thelinefield, and 5 ofthe 7 entries pointed at the wrong line. Two halves, and the second is the
load-bearing one:
bridge.hpp119→121,backend.hpp749→1228,wire.hpp302→307,views.hpp264→271 and430→437.
scripts/check_mutation_survivors.py.The premise, re-measured on
a020e69crather than copied from the issueStill 5 of 7, and
backend.hpphas 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
sourcetext still exists and is still unique, so nodisposition 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'sresolve_allowlist_source_line()directly. That isthe same function hardening
scripts/branch_partial_allowlist.jsonandscripts/error_path_allowlist.json, and its own docstring says it was factoredout to be shared (
check_error_path_coverage.pyis caller two, morph#406). Thisis 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'sdata), before this PR's refresh:
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, thenpython3 scripts/check_mutation_survivors.py: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 — leavesthe gate green on the real file, which is precisely why the self-test has to
be the thing that catches it.
--self-testunder that mutant:(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
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, jobmutation-regression-lint(displayname updated to "Mutation gates (regression self-test, survivor citations)",
since it is no longer only a self-test). Two steps:
Audit the mutation-survivor citations—python3 scripts/check_mutation_survivors.pySelf-test the mutation-survivor citation gate—python3 scripts/check_mutation_survivors.py --self-testUnlike 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 hasno branch protection, so renaming the job's display name breaks no required
check; verified via
gh api .../branches/master/protection→ 404 "Branch notprotected".)
Anti-vacuity, beyond the mutation test
Two things are refused rather than skipped:
document for dicts carrying
file/line/source— so a class a futuretriage pass adds is audited without editing the script — but a restructure
that moved them all would otherwise leave the gate green over zero entries.
file/linebut nosourcefails rather than beingskipped 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 editsourcetomatch 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:
The seven
linevalues are byte-identical in every revision from the file'screation to
a020e69c— nolinehint in this file has ever been edited.registry.hpp:54andlogger.hpp:78are still right only because nothing abovethem 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
runsnarrative. Measured, and theyare stale.
backend.hpp:613is now///and:807a doc comment; the stringemitMetric(Metric::registerCountdoes not occur anywhere inbackend.hppanymore (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
sourceto resolve against — the text after the--is aparaphrase 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_09cited
core/bridge.hpp:121for the HandlerKey hash-combine — correct — whilethe 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.hppandbackend.hpp, which a concurrent lane is actively rewriting. 1228 and 121 mayalready 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
options. Same defect class (a hand-maintained list with nothing auditing it),
different fix site, deliberately left unmerged. Cross-linking only: this
resolver is not reusable there — it resolves a
sourcetext against afile, whereas ci: MORPH_BUILD_BANK_GUI is enabled by no job, and nothing checks that a declared option is built at all #605 needs two sets compared (CMake options vs. a job's
-Dlist). The transferable part is the discipline, not the function.Issues filed
ci: mutation_survivors.json's free-text citations are unaudited, and at least 10 of 31 are stale(bug,documentation,area: ci).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