RT 2026-08-06 - area 15 - fable (Fable 5) - yield 31 #154
ms609-agent
announced in
15 · Legacy pure-R search API
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
fable (Fable 5)| effort: default | 2026-08-06 | third pass — fable arm, run with no exclusionsYield: 31 candidates; ~20 were independent rediscoveries of the opus round's findings (see below — that overlap is the point). Of the genuinely new ones, 12 confirmed and 2 refuted.
Filed: #147 (
sev:med, cross-area —TBRMoves()incompleteness), #155 (sev:med—Ratchet(returnAll=TRUE)reports a wrong MPT set), #156 (sev:low— second sweep of guard, documentation and dead-code defects, including an opaqueSPR()crash).Verification corrected this round's finder three times, so read the issues rather than this post's summaries for the operative detail: the
SPR()crash's stated mechanism was wrong (the proposed fix would have changed nothing) and its count was 6,255 not 138, but it is unreachable from any search so it dropped tolow;Ratchet(returnAll=TRUE)'s start-tree defect has a silent mode the finder never reported, which matters more than the error it did report; and the rooted-dedup defect was upgradedlow→medbecause it inflates a user-facing MPT count. Two candidates were refuted outright and are recorded in #156 so they are not re-hunted.Finder cost: 162,265 tokens / 51 tool uses / ~28 min.
Why this arm had no exclusion block
The opus arm was told everything sonnet found, which measured what opus adds but destroyed the ability to see whether opus would have found sonnet's set anyway — the superset premise the whole ladder rests on. This arm was therefore told only that prior rounds existed, not what they found, and explicitly told not to read
log.md,focus-areas.mdor the tracker.The result: the finder sets are NOT nested, in either direction
Fable independently re-found ~20 of opus's 26 — and both of sonnet's distinctive findings (
RootedNNI(-1)dead,AllSPR's undefinedtree), which opus had been told to skip. That is the superset evidence the opus arm could not produce.But:
SPR()'s guard atR/SPR.R:203isif (edgeToBreak != 1 && all(edgesCutAdrift[-1])), and the!= 1clause exempts precisely the case where the rest of the tree is adrift — 138 hits in an exhaustive n = 5–9 sweep. AlsoMultiRatchet(ratchHits=)silently inert;returnAllnever seeding the forest with the start tree;unique()deduplicating rooted edge matrices so one unrooted topology is reported twice as two "unique MPTs".TBRSwap()'s indefinite hang on a trifurcating root, the SPR neighbourhood-coverage gap, and the unreachablemergeEdgelength guard.So "a higher rung finds a superset" is false as stated. What the data shows is that any competent independent pass finds a partly-different set — which is the rotation argument, not the ladder argument.
Cost comparison (sonnet-equivalents, at 1× / 2× / 4× per-token price): sonnet 137k · opus 360k · fable 649k.
The missing control is #148: a second opus pass with no exclusions, matching this arm's conditions. Without it, fable's marginal contribution cannot be separated from "another fresh pass". Escalation to fable is retained as protocol by maintainer decision regardless.
Headline: a cross-area C++ defect — #147
TBRMoves()omits every TBR move that bisects the root edge, so its output is not a superset ofSPRMoves(). Provable without any oracle, since TBR ⊇ SPR by definition:SPRMoves \ TBRMoves= 8/8/6 on Balanced(8)/Pectinate(8)/random, and 12–14 at n = 11.Two independent defects in
src/rearrange.cpp, both by contrast withall_sprin the same file: the default break sequence starts at edge 3 (:510-513) whereall_sprstarts at edge 2 (:366-369); and there is nobreak_edge == 1branch at all, whereall_sprhas one at:435. Fixing the first alone does nothing.Severity
med, nothigh— the search engine is not affected.all_tbris called only fromR/TBR.R:90,101.MaximizeParsimony()drivessrc/ts_tbr.cpp, a separate implementation that handles the root edge explicitly (try_root_edge_moves,:709,3168). The reach mission is not implicated; exposure is confined to users callingTBRMoves()directly.Two collateral facts:
tests/testthat/test-rearrange.cpp.R:40assertsexpect_equal(58, length(TBRMoves(tr)))— the test currently locks the bug in, and the replacement number must be derived, not copied from the fixed output. Andsrc/rearrange.cpp(22 KB) is in no scope row, so the rotation would never have reached it; it surfaced only because a finder usedTBRMoves()as a cross-check. Third unowned file to produce a real finding (cf. #42).Methodology warning
The obvious topology key does not work.
sort(as.character(as.Splits(...)))is not injective — it collapses the 10,395 unrooted 8-leaf topologies to 8,626, and made the first oracle attempt return 65 instead of 106. Use a bitmask key and validate that it yields exactlyNUnrooted(8) == 10395distinct values before trusting any count derived from it.Conflict to resolve before #143 is fixed
Fable measures
SPRSwapno-ops at 0.1–3.6%; the opus round's verifier measured 9.6–12%. Both were careful. Most likely rooting — that verifier had already flaggedTBRSwap's rate as rooting-sensitive. The rate frames the fix, so pin it first.Ruled out this round — do not re-spend
*Swap()samplers. Fuzzed n = 5–12 × 300 random trees, plus an exhaustiveedgeToBreaksweep n = 5–9: zero invalid edge lists. The swap arithmetic is sound; every defect is in the selection or guard logic around it.TBRSwap()neighbourhood completeness — reaches exactly the full TBR-distance-1 neighbourhood (106/106 at n = 8, nothing outside it). The one sampler that is both complete and essentially no-op-free.RenumberTips(tree, .SearchTipLabels(dataset)). The by-position/by-label bug class does not appear here. One watch point:.SearchTipLabels()returnsnames(dataset), soRenumberTips(tree, NULL)is unguarded for a customTreeScorerwhose dataset is not a named list.stopAtScorepaths are broken.Where to look next
EdgeListSearch⇄RearrangeEdgescontract around the list-returningEdgeSwapperbranch (R/tree_rearrangement.R:42-69) — entirely untested, since no in-package swapper returns a list of lists. Two things look wrong on paper: when all candidates are worse,:69still returns one, and the search regenerates the identical neighbourhood every iteration; andhits <- hits + nBestat:57counts duplicates and input-identical candidates..NonDuplicateRoot()'s choice of which root edge to exclude flips onnEdgeRight ∈ {1, 3}for undocumented reasons, and theSPR()crash lives next to it. It also assumes edge 1 is a root edge —SPRSwap()calls it with no ordering check, unlikeSPR(), whichPreorder()s first.stopAtScorebugs have analogues inResample()/Jackknife().Latent:
Ratchet()'sArgument()helper (R/Ratchet.R:156) resolves viaget(arg)against the enclosing frame — rename any ofsearchIter/searchHits/stopAtScore/stopAtPeak/stopAtPlateauand it silently reaches past the frame into the namespace or globals rather than erroring.Seam status: still yielding — nowhere near dry.
All reactions