red-team area 15: two paired passes over the legacy pure-R search API, plus two inline fixes - #150
Merged
Merged
Conversation
Fixes the one defect small enough to fix inline, and records the round. `EdgeListSearch()` looped `for (iter in 1:maxIter)`. `1:0` is `c(1, 0)`, so `maxIter = 0` silently performed two rearrangement iterations instead of none -- and since `RearrangeEdges()` accepts any candidate scoring `<= scoreToBeat`, a caller asking for zero rearrangements could get a different tree back. `maxIter` is user-facing and reaches this loop from `Bootstrap()`, `Jackknife()` and `Ratchet()`, so `maxIter = 0` is a reachable "score without searching" idiom. Switched to `seq_len(maxIter)`, pre-initialising `iter <- 0L` because `iter` is read after the loop. Pinned by a regression test using mocks that error if called; `EdgeListSearch()` is exported, so it needs no dataset and no C++. The round's other findings are filed as #125 (the documented `edgeToBreak = -1` contract is broken at 4 of 6 exported entry points, one of them silently) and #126 (`SuccessiveApproximations()` runs without the sectorial/fuse/pool machinery, undocumented). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Second pass over the same scope with the sonnet arm's entire yield in the do-not-re-investigate block. 26 candidates, 26 confirmed, 0 refuted -- four of them sev:high, against the sonnet arm's zero. Filed as seven grouped issues: #136, #137, #138, #139 (high), #143, #144 (med), #131 (low). The paired result answers the question the two passes were run to settle: a cheap first pass did not reduce the expensive pass's work, so sonnet-first is an added pass rather than a substituted one. Recorded in log.md; the rationale in focus-areas.md now says not to re-run it. Fixes one defect inline: the "Stability not reached" branch of `SuccessiveApproximations()` emitted its message regardless of `verbosity` while the "converged" branch was gated, so a default `verbosity = 0` call was noisy. Both branches now sit inside one gate. Nothing in tests/, vignettes/, man/ or R/ referenced the string. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The round record has no business waiting on a code review. This file sits on a protected branch, and four completed rounds with 56 filed findings once sat stranded on an unmerged PR while `last_focus:` here still named a stale area -- so the next dispatch would have re-swept an area already reviewed twice that day. Area 15's three rounds now live as one Discussion post each, under that area's category. Removes the two entries added earlier on this branch and replaces them with a pointer. `last_focus:` stays live and stays current: the new scheme picks the stalest category rather than following a pointer, but that ordering cannot be computed until every area has a discussion, and the backfill has to post oldest-first so createdAt reproduces true staleness. Only area 15 is migrated so far. The historical entries below the pointer stay put -- eleven in-repo files and the /red-team skill cite this path, and the T-nnn ids are frozen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Area 15's scope row grew on both sides. Kept as a union, except that cpp-search's "#16 gives this row higher urgency, take it first" is dropped: this branch's round discharged that question (#16 closed, guard landed in PR #50, the pure-R layer measured doubly guarded), so the two cannot both stand and the later assessment wins. cpp-search's src/rearrange.cpp note is kept verbatim. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
All 15 areas have a Discussion record, so rotation reads staleness from createdAt and the pointer is dead. Records the invariant createdAt relies on — creation order equals review-recency order — which the backfill broke and discussion #184 restored. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The header claimed /red-team appends an entry and updates last_focus, which the same file now says is retired. Replaced with what the file actually is. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The `iter <- 0L` added alongside the seq_len() fix was dead: `for` binds its variable unconditionally, so a zero-length sequence leaves `iter` NULL rather than falling through to the earlier assignment. The verbosity report then printed "after rearrangements." with a blank. Handle it where it is read, and cover it — the existing maxIter = 0 test runs at verbosity 0, so it could not see this. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
These rows argued with superseded versions of themselves — why a start_tier was chosen and why it no longer binds, which #42 urgency justified the row and why it is discharged. A scope row briefs the next round; it is not a record of how it came to say what it says. Keeps the operative content: scope, seam verdict, the standing prohibition on re-running the tier experiment, the transferable lesson, and the next-visit leads. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"Performing tree search" was emitted before maxIter was consulted, so maxIter = 0 claimed a search had started and only the closing summary contradicted it. Gate the claim instead, and ask maxIter directly rather than inferring it from the loop variable being left NULL. Co-Authored-By: Claude Opus 5 <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.
Two red-team passes over area 15 (legacy pure-R search API), its first review since the row was created. This PR carries two small inline fixes and the round records; the 31 findings themselves are filed as issues and are not fixed here.
No
Fixes #Nlines: nothing filed is fixed by this branch. The two defects that were fixed were fixed inline and deliberately not filed, per the skill's rule against filing an issue and closing it the same round.Code changes (2 files, 8 lines)
R/CustomSearch.R—EdgeListSearch()loopedfor (iter in 1:maxIter).1:0isc(1, 0), somaxIter = 0silently performed two rearrangement iterations instead of none; becauseRearrangeEdges()accepts any candidate scoring<= scoreToBeat, a caller asking for zero rearrangements could get a different tree back.maxIteris user-facing (default 100) and reaches this loop fromBootstrap(),Jackknife()andRatchet(), somaxIter = 0is a reachable "score without searching" idiom.Now
seq_len(maxIter), withiter <- 0Lpre-initialised — that part is load-bearing, sinceiteris read after the loop at:111.R/SuccessiveApproximations.R— the "Stability not reached" branch emitted its message regardless ofverbositywhile the "converged" branch was gated, so a defaultverbosity = 0call was noisy. Both branches now sit inside one gate. Nothing intests/,vignettes/,man/orR/referenced the string.Test
tests/testthat/test-CustomSearch.Rgains a regression test pinning themaxIter = 0contract.EdgeListSearch()is exported, so it uses mocks that error if called — no dataset, no C++, instant. Verified to discriminate: post-fix the loop body is never entered; with the pre-fix loop bound restored in an isolated copy of the function, it is.Findings filed
Ratchet(stopAtScore=)returns the input tree carrying the improved score, plus two more early-exit bookkeeping failuresRatchet()never forwardsTreeScorerto itsBootstrapper, so the two search phases optimise different objectivesTreeSearch()'s default swapper holds the root fixed, confining a mid-rooted start to ~5% of tree space — needs a design decisionBootstrapTree()/JackknifeTree()to a random walkTBRSwap()hangs on a trifurcating rootedgeToBreak = -1contract is broken at 4 of 6 exported entry points, one silentlySuccessiveApproximations()runs without the sectorial/fuse/pool machinery and never says so31 candidates were grouped into 9 issues by shared root cause rather than filed one-per-finding — the tracker is at 72 open with 3 claimed, and 31 tickets would have made triage worse, not better.
Why two passes
These were the two arms of a deliberate tier-economics experiment: is it cheaper to let
sonnetfind what it can and haveopusfind only the remainder, than to sendopusfirst? Area 15 was the only never-visited area, so a first pass measured the cheap tier honestly.sev:highThe cheap pass removed no work from the expensive one — opus still read all 2,185 loc and found five times as much — so sonnet-first was an added pass, not a substituted one. The severity split is the substantive result: sonnet found broken documented contracts, opus found silent wrong answers. Both
log.mdentries record this, andfocus-areas.mdnow says not to re-run it.focus-areas.mdrow 15 keepsstart_tier: sonnet— the maintainer's recorded choice on #42 — but the rationale beside it now notes that the reasoning behind it is falsified, and that the field is inert for a visited area anyway. One word to change if you want it.Verification notes worth reading
Verification corrected the finder three times, so the issues do not simply restate what the finders reported:
NJTree()is tip-rooted, so this package's own?TreeSearchexample is in the benign regime.NNISwapself-hit rate is2/(nTips-2), not1/(nTips-2)— which changes the fix, since both root-adjacent edges must be excluded;NNI(-1)is missing an entire pair of true neighbours rather than merely padded with duplicates; andTBRSwap()on a trifurcating root hangs indefinitely rather than misbehaving.Both peer verifiers built independent unrooted-neighbourhood enumerators and validated them against the closed forms 2(n-3) and 2(n-3)(2n-7) before using them as ground truth, which is what let them contradict the finders' numbers rather than defer to them. One recorded a trap for whoever rebuilds it: tied n/2|n/2 splits need lexicographic, not size-based, tie-breaking.
Checks
agent-check.ymlgreen onubuntu-arm64andwindows(runs 31014552218, 31037706585).spelling::spell_check_package(vignettes = TRUE)clean. No roxygen or C++ signature changed, socheck_man()andcompile-attrs.Rwere not triggered;vignettes/custom.Rmddocuments this API but usesmaxIter = 50Lthroughout, so the degenerate-input fix changes nothing it describes.🤖 Generated with Claude Code