[AUTOMATED] feat(p8): loopcondhoist - defer a loop head in the ifNoExit scan so ruleBlockWhileDo keeps the head test (+669 GED-perfect / 32,339 O0 functions) - #283
Merged
Conversation
mahaloz
force-pushed
the
feat/loopcondhoist
branch
from
August 11, 2026 18:52
94da39b to
a77e1ce
Compare
This was referenced Aug 11, 2026
…pass over a loop head so ruleBlockWhileDo keeps the head test (+669 GED-perfect over 32,339 O0 functions)
`CollapseStructure::collapse_internal`'s deferred `ruleBlockIfNoExit` scan walks
the live components in order and folds the FIRST one whose terminal arm
qualifies, then restarts the cascade. A head-tested loop whose exit arm is a
`return` is such a component -- once ActionReturnSplit/returndup has given that
return in-degree 1 the rule matches the loop head -- and the head sits ahead of
its own body in component order. So the head folds to `if (!C) return X;` and
drops to ONE out-edge, after which `rule_block_while_do` can never match it and
the loop is emitted as `while( true ) { if (!C) return X; BODY; }` where the
source and IDA write `while (C) { BODY }`.
The `KUNA_RS_DEBUG` trace added in the parent commit shows the alternative is
already in the same scan: on coreutils od read_char and libacl getfacl
walk_tree_visited the loop head is the first candidate and the body block
carrying the `break` is the second. Folding the body first is strictly better --
its clause is the loop follower, which the rule ALREADY requires be reached from
nowhere else (`size_in() == 1`), so absorbing it into the break arm is
semantics-preserving by the rule's own precondition; the body then collapses to
a single back-edge clause and the head test hoists into the `while`/`for` header
on the next cascade pass, with no new machinery and no relocated statement.
`option loopcondhoist on` (default-off opt-in) gives the non-heads one pass of
the scan, falling back to the unrestricted upstream pass when it finds nothing.
A function with no loop-head candidate is byte-identical, and the scan still
terminates on the same fixpoint.
Measured bidirectionally over the whole decbench O0 slice (32,339 scored
functions, 265 slices): 2,281 bodies change, GED 230,291 -> 224,670 (-5,621),
perfect 14,785 -> 15,454 (+669), 680 moved to perfect against 11 moved off
(61.8:1, McNemar z = +25.5). Every architecture gains: x86-64 +542, ARM
Cortex-M +123, i386 PE +4. Over the changed set `while( true )` goes 2,087 ->
223, gotos 621 -> 409, labels 399 -> 251, `// no-return` unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C8UQbPqALzdUQ3cLLjUeKH
… from guardarm #283 was branched on top of #281, whose commit is now on main as a squash. The child was replayed with `git rebase --onto origin/main ee14e46`, so only loopcondhoist's own commit applies -- but the shared counters still needed re-deriving from the BUILT artifact rather than by arithmetic. Two auto-merged at the wrong value with no conflict marker, both because main and the branch had made the identical N -> N+1 edit: catalog_bytecompat.rs (98, must be 99) and xml.rs's corpus file count (196, must be 197). Live catalog 99, phases.toml 99, tiers (20, 44, 35), `},\n` 98, phase_catalog.json 99 (recaptured), docs/options.md 99 (regenerated), kuna-catalog.xml structure-recovery 25. Stages baseline re-recorded on the merged base: 416 -> 419. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C8UQbPqALzdUQ3cLLjUeKH
mahaloz
force-pushed
the
feat/loopcondhoist
branch
from
August 11, 2026 22:24
a77e1ce to
a7b71cb
Compare
mahaloz
added a commit
that referenced
this pull request
Aug 11, 2026
…e stages baseline Rebased onto #278/#281/#283. Every shared counter re-derived from the BUILT artifact rather than by arithmetic: live catalog 100, phases.toml 100, catalog_bytecompat.rs 100, kuna_phases/tests.rs 100 / tiers (21, 44, 35) / `},\n` 99, phase_catalog.json 100 (recaptured), docs/options.md 100 (regenerated), xml.rs corpus 198. `kuna-catalog.xml` structure-recovery stays 25 because calloverlap is a correctness-fix, not a structure-recovery row. Stages baseline re-recorded on the merged base: 419 -> 425, +6 CALLOVERLAP keys, none removed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C8UQbPqALzdUQ3cLLjUeKH
mahaloz
added a commit
that referenced
this pull request
Aug 11, 2026
…call-overlap guards (GH-275) (#284) * [AUTOMATED] fix(p3): calloverlap - complete the two stubbed upstream call-overlap guards (GH-275) Squashed from the branch's four commits (the fix, the two catalog-count bumps, the flip-guidance prose and the post-funcboundflow reconciliation) so the merge train replays one commit instead of four rounds of the same shared-counter edit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C8UQbPqALzdUQ3cLLjUeKH * [AUTOMATED] chore(p0): re-derive the shared counters and re-record the stages baseline Rebased onto #278/#281/#283. Every shared counter re-derived from the BUILT artifact rather than by arithmetic: live catalog 100, phases.toml 100, catalog_bytecompat.rs 100, kuna_phases/tests.rs 100 / tiers (21, 44, 35) / `},\n` 99, phase_catalog.json 100 (recaptured), docs/options.md 100 (regenerated), xml.rs corpus 198. `kuna-catalog.xml` structure-recovery stays 25 because calloverlap is a correctness-fix, not a structure-recovery row. Stages baseline re-recorded on the merged base: 419 -> 425, +6 CALLOVERLAP keys, none removed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C8UQbPqALzdUQ3cLLjUeKH --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
mahaloz
added a commit
that referenced
this pull request
Aug 11, 2026
…e stages baseline Rebased onto #278/#281/#283/#284. Every shared counter re-derived from the BUILT artifact rather than by arithmetic: live catalog 101, phases.toml 101, catalog_bytecompat.rs 101, kuna_phases/tests.rs 101 / tiers (21, 45, 35) / `},\n` 100, phase_catalog.json 101 (recaptured), docs/options.md 101 (regenerated), xml.rs corpus 199, kuna-catalog.xml structure-recovery 26. Stages baseline re-recorded on the merged base: 425 -> 433, +8 orchain keys and the #7 text update, nothing removed. DIV-69 re-checked against origin/main immediately before pushing: main's highest row is DIV-68 (#277), so 69 stands unrenumbered. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C8UQbPqALzdUQ3cLLjUeKH
mahaloz
added a commit
that referenced
this pull request
Aug 11, 2026
…chain of a short-circuit (DIV-69) (#285) * [AUTOMATED] feat(p8): orchain - returndup must not split the operand chain of a short-circuit (DIV-69) DIV-54 flipped `returndup` on evidence from two of the three optimisation levels. Extended to all three (795 slices / 85,195 scored functions) the pass is +640 GED-perfect corpus-wide but -192 at -O0, and one sub-shape carries essentially all of that harm and almost none of the optimized benefit: the split that permanently blocks `CollapseStructure::rule_block_or`. `returndup` gives every predecessor of a shared bare-epilogue RETURN its own private return. For a guard clause that recovers the source; but the operand blocks of a short-circuit expression are predecessors of that same block, and the shared out-target IS ruleBlockOr's entire precondition. ActionReturnDup runs in fullloop's `returnsplit` group and collapse_conditions runs later, so the fold is lost for good and one source `return a || (b && (c || d));` prints as a five-return constant-guard cascade (iproute2 ip::sci_complete, GED 21 against 0). New option `orchain on|off` (default ON, DIV-69, ELEM 4122). `kuna_orchain.rs (shortcircuit_shared_targets)` replays collapse_conditions read-only on the bblocks CFG, mirroring ruleBlockOr's own admission test rather than approximating it (one in-edge, two out-edges, not a switch, not BlockBasic::isComplex, a shared out-target, sibling's other target is not the head), and `returndup_apply` declines the whole function's splits when one of its own candidates is a recorded target. It also protects a block joining two or more single-exit arms that are themselves fold targets, because the first returndup invocation of the fullloop still sees the constant-materialisation blocks in between. Sweep over the same corpus (base = shipped defaults at merge-base 82dd39a): O0 +611 GED-perfect / -2,816 GED, O2 -13 / +1,232, O2-noinline -15 / +617 -- +583 net and -967 aggregate, 781 functions to perfect against 198 off (3.94 : 1). That is 92% of the +630 the post-hoc "a short-circuit was lost" signal prices as a ceiling, at a smaller optimized-level cost than the ceiling itself (-28 vs -35). Predicate fidelity to that signal: 2,933 of 2,951 O0 positives recognised (99.4% recall) at 126 further firings (95.9% precision); 94.2% of firings reproduce the `option returndup off` body byte for byte. Default-ON rather than an `aggressive` preset member because `returndup` is a shipped default in every mode and 45% of the corpus (38,216 of 85,195 functions, 36 of 795 slices) is over the 500 KiB `auto` threshold and runs `reliable`, where a preset-only gate never runs -- measured: with the gate in the preset only, the no-option arm reproduced the ungated body on iproute2 ip (76 functions), bash (366), sshd (201). 675/675 PARITY OK with the raw flip, zero assertion movement and no per-test opt-out; stages 415/415 with the new two-pass tests/stages/ghdec-orchain.xml; rust-test 4,540 passed / 0 failed; check-spec OK lenient + strict; catalog --check OK. Speed worst +1.01% (two of four probes are net speedups). Rebased onto #280 (funcboundflow, DIV-67): every shared counter re-derived from the built artifact on the rebased tree (97 settables, tiers 20/43/34, xml corpus 195), and DIV-68 was renumbered to 69 after Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C8UQbPqALzdUQ3cLLjUeKH * [AUTOMATED] chore(p0): re-derive the shared counters and re-record the stages baseline Rebased onto #278/#281/#283/#284. Every shared counter re-derived from the BUILT artifact rather than by arithmetic: live catalog 101, phases.toml 101, catalog_bytecompat.rs 101, kuna_phases/tests.rs 101 / tiers (21, 45, 35) / `},\n` 100, phase_catalog.json 101 (recaptured), docs/options.md 101 (regenerated), xml.rs corpus 199, kuna-catalog.xml structure-recovery 26. Stages baseline re-recorded on the merged base: 425 -> 433, +8 orchain keys and the #7 text update, nothing removed. DIV-69 re-checked against origin/main immediately before pushing: main's highest row is DIV-68 (#277), so 69 stands unrenumbered. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C8UQbPqALzdUQ3cLLjUeKH --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes symptom B of the round-4
ifnoexitcluster(
docs/decbench/features-round4.mdRank 2). The filed root cause there isrefuted (Killed K7) and the filed "ship form A first" plan is refuted
(Killed K8); neither is what this ships.
Phase 1 — the decision trace, and its verdict
Three independent refuters asked for one experiment: a decision trace inside
CollapseStructure::rule_block_if_no_exit. It ships in this PR(
p8_structure/kuna_ifnoexit.rs+ theKUNA_RS_DEBUGprints inblockaction.rs), and the verdict is the deliverable, independent of the twofixes. Reproduce with
KUNA_RS_DEBUG=1 kuna decompile-all <bin> --addr 0x...— notekuna decompilespawns
decomp_dbgwith piped stderr and swallows the trace, sodecompile-allis the surface that shows it.
Per candidate it prints
bl's type / degree / start address, both arms'size_in,size_out,is_decision_outand exit-leaf halt type (testing thepcodeop_flags::noreturnbit,0x1000000, not thereturn(#0x1:4)shape —badinstruction/unimplemented/missing/haltare identical in shape),whether
blis a live loop head, whetherrule_block_while_dowould fire rightnow, the whole deferred-scan order, and the chosen
i. Theapply#Ncounter isin every header because
collapse_allruns twice per function.1. Is A an arm predicate, a scan-order predicate, or both? — a pure arm
predicate, and only in a tie. On
dpkg-query control_listboth guards arein=1 out=0 dec=trueon both arms, the rule takesi=0in both, and the onlydifference between the guard that is right and the guard that is wrong is which
side the halt happened to land on:
When only one arm is eligible the
for i in 0..2loop already picks it, which iswhy ~84% of the filed signature population is already right and is untouched by
any tie-break. The scan order is irrelevant to A: the same block is the only
candidate either way.
Why out-index carries no source information. The trace also settles the
"filed root cause predicts the opposite of the output" contradiction the refuters
logged on
scp xcalloc(objdumpputs the fatal block on out(0), yet kunaemits the inverse).
apply#0picks the halt arm ati=0correctly — andthat pick negates the condition,
take_pending_flips->block_basic_negate_lastop->swap_edgesre-orients the block, andapply#1sees the arms swapped and picks the
return. The second run undoes the first.The orientation at the rule is not the disassembly orientation.
2. Is B the same site, and does fixing the scan order alone fix A? — same
60 lines, different predicate, and no. On
coreutils od read_charandlibacl getfacl walk_tree_visitedthe trace showsloophead=yes,whiledo_would=no, and exactly one eligible arm — the tie-break never runs,so nothing an arm predicate does can touch B. Conversely the guards in A are
loophead=noand are the only candidate in their scan, so nothing a scan-orderpredicate does can touch A. They are disjoint on every witness.
Two further facts fall out of that dump. The better candidate is already in
the same scan, one position behind:
#2@0x5340, the block carrying thesource's
break, is itself ifNoExit-eligible (its arm#3@0x53a4isin=1 out=0) — first-match in component order simply reaches the head first.And the rule only becomes eligible on the head after the return split: in
apply#0the sharedreturn v3block isin=2, the rule declines, and nothingfires;
ActionReturnSplit/returndupthen gives it in-degree 1 andapply#1folds the head. That is why B is an -O0 population, and it is also why the filed
region_structurer.rs:1788 cb.size_out() != 1diagnosis (Killed K7) could neverhave been the site.
3. Which must land first, and does either regress the other? —
guardarmfirst, and no. They are separate predicates on separate inputs, so they are
two PRs.
guardarmis the narrower and more obviously symmetric change (the tieis between two folds that are both legal;
new_block_ifis symmetric in the armit takes), so it goes first and
loopcondhoiststacks on it. Measured, notassumed: the four named counter-cases are byte-identical under each option
alone and under both together, and the two options' changed-function sets
were measured separately on the same corpus.
4. What separates the ~84% already-correct panes from the inverted ones?
The tie itself, plus code layout. It must distinguish
control_list's twoguards from each other, and the halt bit cannot — the trace above shows both
guards carry
halt=noreturnon one arm. What differs is which arm, and thecompiler's own answer to that is the address: an unoptimised compiler emits the
taken clause of
if (c) A; B;in front ofB, and thethenside of anif/else in front of the
elseside. So: in a tie, the arm whose front leaflies earlier in the address space becomes the clause. This deliberately does
NOT read
op_mark_halt— preferring the no-return arm fixesscp xcallocandcontrol_listand invertscoreutils make-prime-list xalloc, whose sourcereally is
if (p) return p; fprintf(...); exit(1);(
O0/coreutils/compiled/make-prime-list.i:3645). Layout gets all three right.5. For B form C, what is the exact in-code follower assertion? — it is
already in the rule:
size_in() == 1. The round required "the loop'simmediate structural follower, in-degree exactly 1" and warned that a matcher
keyed on "a
return Xreachable after the loop" freeslibacl getfacl get_list's list and returns NULL. No new predicate is needed, because the fixis not a hoist-and-relocate:
rule_block_if_no_exitfolds the body blockwhose clause is the follower, and its own
if size_in() != 1 { continue; }is exactly that assertion.
get_list's follower issub_27de(v9); return NULL;— not a bare return, reached only by the
break— so it moves into the breakarm and the normal loop exit returns the list, which is the baseline's semantics
exactly. Verified on the rebased tree:
bzip2 mainGtUis dropped from the witness list as the round instructed: itis a non-member (its source is a genuine
do/while).Phase 2 — what
loopcondhoistdoesGive the non-loop-heads one pass of the deferred
ruleBlockIfNoExitscan;fall back to the unrestricted upstream pass only when it finds nothing.
Loop-head-ness is resolved through the collapsed graph exactly the way
LoopBody::updateresolves a recorded head. A function with no loop-headcandidate is byte-identical, and the scan still terminates on the same fixpoint
because every fold strictly reduces the component count.
No new transform, and no follower-move predicate — see verdict item 5: the
in-code assertion the round demanded ("the loop's immediate structural follower,
in-degree exactly 1") is
rule_block_if_no_exit's ownsize_in() != 1test,which the existing rule already enforces.
loopcondhoist oncoreutils od read_char@0x5320while( true ) { if (!dat) return v3; ... }while (dat_12198) { ... }libacl getfacl walk_tree_visited@0x44dfwhile( true ) { if (v1 == a0) return 0; ... }for (v1 = *(a0+8); v1 != a0; v1 = *(v1+8)) { ... } return 0;— the source verbatimp23(form C)while( true ) { if (v1 <= 0) return 0; ... break; }for (v1 = a1; 0 < v1; v1 = v1 + -1) { if (...) return 1; } return 0;— the source; this is the new stage testp7,p20Named counter-cases, verified on the rebased tree
Re-run on
feat/loopcondhoist@a77e1ce5(rebased ontoee14e461/11f40f46):bash/mksyntax main@0x19f7coreutils/tail xlseek@0x4804coreutils/make-prime-list xalloc@0x1906libacl/getfacl get_list@0x281aget_listis the named victim of a naive follower-relocation, and it is the onecounter-case that does change shape — correctly:
The
free+return NULLmoves into thebreakarm and the normal loop exitreturns the list. It does not free the list it just built.
mksyntax mainis the
form Acounter-case (Killed K8) and is untouched because form A comesfrom the
is_complex(head)overflow-syntax branch, which this predicate doesnot go near.
bzip2 mainGtUis dropped from the witness list per the round's owninstruction: its source is a genuine
do/while, so it is a non-member.Measurement — both directions
Full O0 slice (pre-rebase,
82dd39a7base)265 slices, 32,339 functions scored in both arms. Controls clean: 30,058
unchanged bodies, 0 scoring differently.
This is the largest single result in the round, it beats the round's own
estimate (~210 functions / ~1,234 GED) by 3x, and no architecture loses.
Re-measured on the rebased tree (#280
funcboundflowis upstream of P8)funcboundflowchanges function boundaries, so the sweep was re-run ona77e1ce5over a representative 7-project slice spanning all threearchitectures and containing every previously-recorded mover:
openssh-portable, betaflight, coreutils, dpkg, mydoom, libacl, zlib —
139 slices, 16,019 functions.
Identical, function-for-function — the single off-perfect is
libacl/setfacl::has_execute_permsin both. Post-rebase headline for the slice:1,284 bodies changed, GED 123,837 -> 120,233 (-3,604), perfect 6,817 -> 7,267
(+450), 451 / 1 (z = +21.17); per arch x86-64 +405, ARM +42, PE +3.
The mirror population, by name
All 11 full-corpus off-perfect functions (
moved.csv); every one is x86-64,and there are zero on ARM or PE:
libacl setfacl has_execute_perms— the only one in the re-measured slice — wasread against its source (
O0/libacl/compiled/do_set.i:5323), which is a literalfor(;;)with two internalreturns. The baseline renders itdo { ... } while (...)(GED 0) and the variant renders itwhile( true ) { ... if (v1) break; ... } return 1;. The variant is the more faithful of the two and GED charges5. Worth naming as an honest limit: on a genuinely-infinite source loop the pass
can move toward
while( true )rather than away from it.Requirement 8 — every changed function, not the witness
All 2,281 changed bodies audited mechanically:
while( true )// no-returnSixty-six functions lose duplicated callees; all were inspected and they are the
returndup-cloned loop follower being re-shared as the single loop exit.openssh addr_match_cidr_listis the clearest — five copies offree(v11); return v9;become one after the loop with fourbreaks, which isthe openssh source (
for (...; (o = strsep(&cp, ",")) != NULL;) { ... goto foo; } foo: free(list); return ret;) verbatim.Speed
Interleaved min-of-N whole-binary
decompile-allon the rebased build. Noregression; plausibly a genuine speedup, since 1,864 fewer
while( true )structures means far fewer
select_goto/TraceDAGrounds.loopcondhoist oncoreutils ls(min-of-5)openssh sshd(min-of-3, 9,173 fns)Both options together stay inside the 5% budget on the largest binary in the
corpus. (An earlier loaded run read
lsat -26%; that was contention, not aspeedup, and does not reproduce.)
Gates — run on the REBASED tree (
a77e1ce5, baseee14e461on11f40f46)Exit codes captured directly (
make ... > log 2>&1; echo $?), never through apipe to
tail.docs/baseline-stages.jsonmoved 407 -> 413 and the diff was inspected key bykey: +6 assertions (3
GUARDARM, 3LOOPCONDHOIST) and theKUNA-CATALOG #7key renamed because its assertion name embeds the option list. Nothingremoved, no existing verdict changed.
Every hard-coded catalog count re-derived from the built artifact, not by
arithmetic, and grepped back out of the file to prove the edit is not a silent
no-op. (#280 and #278 made the identical
95 -> 96edit at four sites, gitauto-merged them with no conflict marker, and
catalog_bytecompat.rsneverappeared in
git status.) Read assertions, not test names — onmainsettable_count_is_95asserts96.kuna catalog --jsonoptionskuna_phases/tests.rskuna_num_settables/SETTABLE_TABLE.lenemit_catalog_json},\ncountcatalog_bytecompat.rsfixture"option"/"tier"/"symptoms"kuna-catalog.xml#7structure-recoverykuna-base/src/xml.rscorpus filesphase_catalog.json"option"entriesdocs/options.mdandtests/fixtures/phase_catalog.jsonregenerated from thatbuild;
docs/baseline-stages.jsonre-recorded, not hand-merged. Two otheroption-adding PRs are in flight (#282 P9, a P3
calloverlap) plus an or-chainagent in
p8_structure, so expect these counters to need re-deriving againat merge time — re-derive, do not add.
Scope
Default-OFF opt-in,
tier = transform. No DIV row: a new default-offoption changes no default (DIV-67 is claimed by #280, DIV-68 by #277; next free
is DIV-69, and neither of these two PRs takes one). Listed in
modes.rs'sUNEVALUATEDwithguardarm.Given +669 perfect with no architecture losing and an 11-function mirror
population, this is the stronger of the two candidates for the preset flip —
that PR's remaining work is the 0/675 ablation with the flag forced on and a
preset-level speed number.
Spec prose:
docs/spec/08-structuring.mdsection 8.1.Merge order
Intended order for this pair:
feat/guardarmtomainfirst:gh api -X PATCH repos/Noelo-Lab/kuna/pulls/283 -f base=main(
gh pr editis broken in this environment; use the API form.)guardarm) ->main.main.delete_branch_on_mergeis set repo-wide, so merging #281 deletesfeat/guardarmand auto-closes this PR unless step 1 happens first.Sequencing against the other option-adding PRs in flight. #278 and #284 both
land the catalog on 97 and are mutually exclusive at that value; #282 (P9) and
the P3
calloverlapadd options too. Whichever of the five merges after anotherneeds its counters re-derived from a green build, not incremented. Concretely
for this pair: #281 asserts 97 and #283 asserts 98 against today's
main(96). If any other option-adding PR merges first, both numbers move andmust be re-derived — the four sites that merge clean at a wrong value are
catalog_bytecompat.rs(itsfixture_has_all_N_settables+ three.matches(...).count()asserts), thekuna_num_settables/SETTABLE_TABLE.lenpair,
emit_catalog_json_static_form_brackets_and_commas's trailing-comma tally,and
tier_counts_are_*.🤖 Generated with Claude Code
https://claude.ai/code/session_01C8UQbPqALzdUQ3cLLjUeKH