Severity: P1 · Area: 10 (HSJ/XFORM kernels)
Location: src/ts_hsj.cpp:140-161 (alpha·d/m term), src/ts_sankoff.cpp:74-85 (min-over-root-states), R/recode_hierarchy.R:176; trigger site ts_collapse_pool in src/ts_rcpp.cpp
One fixed unrooted 10-tip topology scored under each of its 10 tip-rootings: EW 28 ten times (invariant); HSJ α=1 → 24.5/25; HSJ α=0 → 20 ten times. So the dependence is entirely the alpha·d/m secondary term — fitch_label_char's DELTRAN-style directional uppass resolution, whose comment at ts_hsj.cpp:77-91 justifies invariance to level ordering only and is silent on rooting. Across 300 random topologies, XFORM is rooting-dependent on 165/300 and HSJ on 197/300. The wrong-answer symptom (seed 20260728): reported best 15.5, but TreeLength() of the 6 returned MPTs gives 16 15.5 15.5 16 16 16. Causation pinned by three controls, which localise the trigger more precisely than the original finding did: (i) collapse = FALSE → all six give 15.5, and pairwise RF shows both runs return the same six unrooted topologies (every row and column of the cross-RF matrix has a 0) — only the rooting differs; (ii) it is per-tree, not cross-tree — the topology that returns 16 scores 15.5 under all 7 of its other edge-rootings, so the single rooting ts_collapse_pool imposes is uniquely pessimal for it; (iii) scorer mismatch is excluded — R/tree_length.R:183-189 and R/MaximizeParsimony.R:1220-1222 use the same .NonHierarchyWeights, .HSJAbsentState, .HierarchyToBlocks and alpha. Falsified code assertion: the comment above ts_collapse_pool in src/ts_rcpp.cpp claiming the tip-0 re-root is "what makes the contraction rooting-invariant" is false for HSJ/XFORM. Why P1 regardless of design intent: a reported best score that cannot be reproduced from the returned tree is a wrong user-visible result, and 4 of 6 trees returned in one MPT set do not share a score under a common rooting. DESIGN QUESTION SETTLED FOR HSJ, 2026-07-28, FROM THE PAPER — the rooting-dependence is a BUG, not an intended rooted objective. Hopkins & St John (2021) define the score as a minimum over internal-node labelings of a sum of dissimilarities over branches (p.3: "for a fixed phylogenetic tree, we extend the character labelings of the leaves to the internal nodes of the tree and compute the minimal score"; p.6: the algorithm keeps "both the possible score when the controlling primary character is present and when it is absent" and "return[s] the minimal score"). The HSJ dissimilarity is symmetric in its two endpoints (d = "the number of nonmatching secondary characters", p.5), the branch set of an unrooted tree is rooting-independent, and a minimum over labelings introduces no orientation — therefore the objective is rooting-invariant by construction. Corollary for the fix: the paper prescribes a two-state DP carrying both present/absent possibilities and minimising; fitch_label_char's single directional DELTRAN-style uppass commits to one resolution, which is neither the paper's algorithm nor guaranteed minimal, and is the source of the α-term dependence measured here. So the fix is to implement the paper's DP (or equivalently resolve on marginal MPR sets), not to pin a rooting. XFORM HALF NOW DECIDED, 2026-07-29 — decision document: dev/plans/2026-07-29-t374b-xform-rooting-policy.md (branch claude/t374b-xform-rooting-policy; evidence dev/red-team/heavy-tests/xform-rooting-oracle.R, pure R, no build, exits 0 while its predictions hold). The earlier reading — "an asymmetric step-matrix character is intrinsically rooted, so pin a rooting and stop sankoff_forced_root being -1, which needs TBR rethought" — is superseded. Recommendation: keep the objective unrooted, do NOT pin a rooting; fix the reporting discrepancy instead. Three results drive it, and the lead evidence is (2), not (1). (1) The tempting cheap fix — forced_root_state = 0 ("absent at the root") — is rejected as INCOHERENT ON ITS OWN, not as harmful. It does not restore rooting-invariance: the pinned criterion's value varies across root positions on 84–117/120 random 9-tip topologies, spread up to 5. Read that carefully — pinning the root state defines a different, explicitly rooted criterion which the oracle confirms is perfectly well-defined at any fixed rooting, so its spread is the price of leaving its root position arbitrary and is not commensurable with the free-root arm as "worse". (An earlier draft of this row over-claimed a refutation on exactly that bad comparison; likewise the nSec = 0 pinned cell, 84/120, is a tautology — constraining the root's label on a symmetric matrix must make the root position matter — and is a valid asymmetry control for the FREE arm only.) What it does establish: shipping the one-liner into today's pipeline, which leaves the root position unchosen and moves it at four sites, makes the score depend on a root the user never selected by up to 5 steps. So pinning the state is only meaningful together with pinning the position — i.e. it is Option 2, not a one-liner. (2) The dependence is bounded and one-directional — this is what actually licenses the recommendation: the asymmetric part of the cost matrix is a gradient (f(absent)=0, f(present)=nSec/2), so total = Σ_edges s − Σ_internal f − f(root) + Σ_tips f; the root is charged f twice, giving a tested spread bound of nSec per block (an earlier tighter nSec/2 guess was falsified by the oracle — rooting subdivides an edge, so Σ_edges s is not itself rooting-invariant). An arbitrary rooting always overstates the well-defined min-over-rootings objective; 87–98% of rootings attain it, mean overstatement 0.02–0.17 steps — so the search optimises a sound but loose upper bound, not a wrong number. Fully ambiguous tips (-1) are the aggravating factor (~3× the dependence rate, 8× the overstatement); present-unknown (-2) is not. (3) TBR is NOT the obstacle — the "rethinking TBR" framing conflates two rerootings. TBR's fragment reroot is a genuine unrooted-topology change and is compatible with a pinned root; the default !phys_reroot path never physically reroots the whole tree (ts_tbr.cpp:2967-2984 enumerates root-edge moves instead). Whole-tree rerooting happens at just four enumerable sites: ts_tbr.cpp:2997 (legacy, TS_PHYS_REROOT only), ts_fuse.cpp:20 (called :344/:381/:398), ts_rcpp.cpp:2088 (ts_collapse_pool, the T-374 trigger), ts_sector.cpp:1101. The min-over-root-states at ts_sankoff.cpp:74-87 is therefore not sufficient (it frees the root's state, not its position); the correct unrooted objective is min over rootings and states, affordable on the report path only ((2n−3)× the Sankoff term). Recommended fix = make MaximizeParsimony's reported score and TreeLength() agree on one rooting (prefer min-over-rootings on the returned pool, which also makes the MPT set self-consistent), and document XFORM as rooting-sensitive with the nSec-per-block bound. Do not require rooted input or error out — XFORM has no root to demand. A/B verdict: do NOT run the rooting-pinned A/B (the only cheap pinned variant is the incoherent one from (1), so a score difference could not distinguish "pinning helps" from "that arm drew a favourable root"; the effect is second-order behind T-377, whose Fitch-only candidate screen would confound it; and accepted scores are already authoritative via full_rescore at a rooting that is stable within a TBR pass — incoherence enters at the four sites above, not in TBR's accept loop). Two further false root-invariance comments to annotate on fix: ts_tbr.cpp:123-124 and ts_tbr.cpp:2999, alongside the already-recorded ts_collapse_pool one (ts_tbr.cpp:802-818 is correctly root-dependence-aware and needs no change). The gating blocker is the already-filed T-377 (TBR's candidate scan is hierarchy-blind): the Sankoff term is absent from XFORM's TBR candidate screen, which is first-order and must be settled before any rooting measurement is interpretable. T-374b re-derived that independently from code and briefly mis-filed it as a new T-383 before finding T-377; the duplicate row is deleted and T-377 now carries the three refinements (the has_na-conditional convergence sweep, the over-permissive dominated test, and this A/B gating). A paper-derived fix-gate is committed at dev/red-team/heavy-tests/hsj-paper-oracle.R. Not reproduced, and not load-bearing: the XFORM MaximizeParsimony symptom (0/6 seeds — mechanism confirmed, symptom did not surface as those searches returned one tree each; the finder claimed this for HSJ, not XFORM) and the all-hierarchy 7.5 vs 8 case (analogue gave 3/3). A third consequence worth carrying: because the objective wanders with the rooting during search, TBR's own accept/reject comparisons may be incoherent across moves — unquantified; a matched A/B against a rooting-pinned variant would settle it. Verified REAL by opus verifier at pinned 1a94403b. XFORM half REPRODUCED against tip a8fbba84 on 2026-07-31 (T-385), and the decision document's Q5 is INCOMPLETE — read this before implementing it. Repro dev/red-team/heavy-tests/t385-xform-report-agreement.R (36 tips, 6 blocks, nSec = 2, seed 1 / search seed 11, maxReplicates = 4): reported attr(res, "score") = 178; TreeLength() of all 32 returned trees = 183 (gap 5); the same topology under 8 rootings spans 178-183 (spread 5, within the Sum nSec = 12 bound); and the 32 "equally parsimonious" trees carry 2 distinct scores (183, 182) at a common tip-1 rooting. The reported number is not wrong — 178 is attained by 2 of the 8 rootings; the engine records a score at one rooting (R/MaximizeParsimony.R:1672, score = result$best_score) and returns a topology re-rooted elsewhere by ts_collapse_pool (:1600-1630), whose :1604 comment asserts the very root-invariance this finding refutes. RETRACTION of this row's first refinement (same day, before any fix was written): I claimed the MPT-set symptom survives a canonicalised report path, on a measurement of "2 distinct scores (183, 182) among 32 MPTs at a common tip-1 rooting". That measurement was invalid and the claim is withdrawn. It rooted each tree at its own tip.label[1], which after Renumber() is a different taxon for different trees -- so it was a different rooting per tree, not a common one. Re-measured properly (dev/red-team/heavy-tests/t385-diagnose-rooting.R): all 32 returned trees are already rooted at the kernel's tip 0 (atTip1 32/32, root degree 2) and at a genuinely common rooting all score 183 -- 32 distinct topologies, 0 disagreement, score range 183-183. So the returned pool is self-consistent, and the MPT-set symptom this row records from the earlier session ("4 of 6 trees do not share a score") did not reproduce here; it is not evidence against canonicalising, and pool re-filtering is not required. What survives, and is the P1: the reported score (178) is not the score of the tree at the rooting it is actually returned at (183) -- R/MaximizeParsimony.R:1672 reports result$best_score from search state while ts_collapse_pool returns every tree at the tip-0 rooting. Also real: the same topology spans 178-183 across 8 rootings, i.e. the objective is genuinely rooting-sensitive within the Sum nSec = 12 bound (178 is attained by 2 of the 8, so the reported number is a valid value at some rooting, just not the returned one). XFORM reporting half FIXED (T-385), and the MPT-set residue is now CONFIRMED REACHABLE and no longer silent. Fix on claude/t385-xform-report-agreement (stacked on PR ms609#277): TreeLength() canonicalises the rooting in BOTH its single-tree and multiPhylo methods (the latter previously rooted only trees that arrived unrooted, so an already-rooted tree kept its own rooting), and MaximizeParsimony() rescores the returned pool through that same path before reporting. Acceptance script goes gap 5 -> 0 and rooting spread 5 -> 0. The residue: MaximizeParsimony() now warns when the returned trees do not share the canonical length, and that warning fired immediately on an existing test -- test-ts-xform.R's "Xform search handles all-hierarchy data (zero Fitch words)" returns a pool spanning 7 to 9. So this row's original "4 of 6 trees do not share a score" IS real and reachable (all-hierarchy matrices), even though my own 36-tip re-measurement of it was invalid (retracted above). Cause is unchanged and still open: pool membership is selected at R/MaximizeParsimony.R:1618 on search-time scores taken at differing rootings. Fixing it means canonicalising pool selection, which moves n_topologies semantics and interacts with collapse -- unanalysed by the decision document, deliberately out of T-385's scope. HSJ reporting deliberately NOT canonicalised at the REPORTING boundary: there rooting-invariance is required by the method, so canonicalising the report would convert a wrong objective into a stably-wrong one and burn this row's HSJ half. HSJ HALF FIXED AND MERGED 2026-08-02 as 34eea581 (PR ms609#281, feature/hsj-rooting-invariant-dp, branched off 93c81a9a). CI on the merge: sense-check and both Linux matrix jobs green; windows-latest fails at "Set up R dependencies (covr)", which fails identically on the base branch and is unrelated. FIRST, A CORRECTION THIS ROW ITSELF PROPAGATED, AND WHICH COST A ROUND OF WORK: the claim that fitch_label_char's directional pick is 'neither the paper's algorithm nor guaranteed minimal' conflated two different things, and the prescription 'implement the paper's two-state DP' was already satisfied. score_hierarchy_block()'s a(n)/p(n) recurrences ARE Algorithm 1 (p.15) lines 6-7, term for term; the two-state DP was implemented and was ALREADY rooting-invariant, because its branch costs are symmetric and it minimises over the root's own state. The defect was never in the DP -- it was that d(u,v) was read off the SECONDARY labelling. Nor would copying Algorithm 1 literally have helped: its line 2 sets L(n) to the first-pass Fitch labelling, i.e. the DOWNPASS, which is root-dependent, and line 8 updates L(n) in postorder; Algorithm 1 takes "Tree, T, with root r" as INPUT, so Theorem 2's claim that it returns the minimal score cannot hold as stated -- if it did, its output would be rooting-invariant, and the measurements below show it is not. THE MEASUREMENT THAT COLLAPSED THE DESIGN SPACE. Under ANY most-parsimonious reconstruction of secondary j, the number of branches on which j changes equals FitchLen_j. So for an ALL-PRESENT block the alpha term is (alpha/m) * sum_j FitchLen_j EXACTLY, whichever labelling the uppass picks -- verified 50/50 on random data, and it reproduces Fig. 1 exactly (6+alpha / 3+2alpha, i.e. the paper's 7 and 5 at alpha = 1). The rooting-dependence therefore lived ENTIRELY in blocks with MIXED present/absent primaries, measured over every edge-rooting of 30 random 9-tip topologies at alpha = 1: all-present 0/30 at 93c81a9a already; mixed 21/30 (m=2) and 26/30 (m=4), spread 1.00; ?-primary 6/30 and 18/30, spread 0.50. Note the pre-existing figures in this row (165-197/300, "24.5/25 across 10 tip-rootings") were taken at 1a94403b and are superseded; the oracle's "2 pass / 3 fail" is likewise stale, PR ms609#280 took it to 8/0. TWO MECHANISMS, BOTH FIXED. (1) Semantic, and the dominant one. "-" was admitted as an ordinary state of a secondary character. Where a controlling primary codes the structure absent its secondaries do not exist, so "-" there is not a state the character takes; admitting it let the uppass propagate "-" INWARDS and label a node in the middle of the PRESENT region inapplicable, where it is disjoint from every present neighbour in every secondary at once and the branch was charged d = m, the full alpha. That over-charge is wrong under ANY rooting (the paper's d counts nonmatching secondaries among characters that APPLY, p.5), so this is a correctness fix that removes most of the rooting spread as a side effect, not a rooting fix. Note the comment formerly at ts_hsj.cpp:45-47 asserted this defect as deliberate design. Secondaries are now wildcarded at tips whose PRIMARY token can mean absent -- keyed off the primary, not off the secondary's own token, because a "-" secondary at an unambiguously-present tip is contradictory data that ValidateHierarchy rejects upstream. Alone this took mixed to 3/30 and 7/30 (spread 0.50/0.25) and ?-primary to 1/30 and 3/30. (2) Directional. The remaining resolution was a DELTRAN uppass whose direction, and tie-break support counts whose SUBTREES, were properties of the input rooting. The labelling passes now run over a traversal rooted canonically at tip 0, built once per block; neighbour lists are sorted by node index so the traversal cannot inherit the incoming orientation, and the kernel's own root becomes an ordinary degree-2 node that Fitch passes through transparently. Result: 0/30 in every regime, spread 0.0000. THE HEADLINE SYMPTOM IS CLOSED. Twelve HSJ searches on 16-tip mixed matrices: 3/12 returned trees whose TreeLength() disagreed with the reported score, worst gap 0.75, one pool spanning 31.25/31.5/31.75/32 -- at 93c81a9a AND after mechanism (1) alone. Now 0/12, every pool internally consistent. So the semantic fix alone did NOT close this row; the canonical rooting was needed. WHY CANONICAL ROOTING, AND WHY IT IS NOT PR ms609#278 AGAIN. The exact minimum over labelings is the inapplicable-Fitch problem (min over primary labellings of boundary-count + (alpha/m) * sum_j FitchLen_j over the present forest) and is not available in linear time; the per-branch min-over-MPR relaxation is REFUTED (a 0,1,0,1 caterpillar gives sum of min = 0 against min of sum = 2). A composite Sankoff over {absent} + all secondary combinations with the symmetric HSJ cost matrix IS exact and IS fully rooting-invariant -- the matrix satisfies the triangle inequality, so even edge-subdivision is safe -- but it is exponential in nSec and is what the XFORM path already does, so it would delete HSJ's reason to exist alongside XFORM (O(nk)). The maintainer was given these three options with the measured residual attached and chose the canonical rooting. It is distinct from PR ms609#278 in kind: that canonicalised at the REPORTING boundary, leaving search on a root-dependent objective; this sits INSIDE the kernel, so the objective the SEARCH optimises is a function of the unrooted topology and MaximizeParsimony agrees with TreeLength by construction, with no rescore. It buys INVARIANCE, NOT EXACTNESS -- do not record this row as "HSJ now computes the paper's minimum". TESTS, six in test-ts-hsj.R, verified against a build of 93c81a9a in a throwaway worktree (use testthat::set_max_fails(Inf) -- the default cap of 10 silently hid the last test on the first attempt): rooting invariance on a fixed mixed block 2 failures (alpha = 0 passes, as the control locating the dependence in the alpha term), on 8 random matrices 6, "-"-vs-"?" 5, MaximizeParsimony pool 5. Two are regression FLOORS that pass either side (Fig. 1 = 7/5; the all-present closed form). Two had to be rebuilt before they tested anything: the p.5 <= 1 per branch bound is far too loose summed over a tree and PASSED pre-fix, replaced by the sharper invariant that "-" and "?" at an inapplicable secondary must score alike; and the MaximizeParsimony test was maxSeconds-bounded and flaky in BOTH directions, now maxReplicates-bounded on a construction where 14 of the first 40 seeds are discordant pre-fix. hsj-paper-oracle.R now discriminates: 12 pass / 1 fail at 93c81a9a (only the new mixed-block check [3b] fails, 9/12, worst spread 0.667) against 13 / 0 after, with every other check identical across the two builds. Its rooting check now sweeps all 2n-3 edges rather than n tip-rootings. COLLATERAL: the four false root-invariance comments are narrowed to XFORM rather than deleted (ts_tbr.cpp reroot_at_tip contract and reroot-cycle note, ts_rcpp.cpp ts_collapse_pool, R/MaximizeParsimony.R collapse note); ts_tbr.cpp:802-818 was already correct and is untouched; the XFORM rescore at MaximizeParsimony.R:1709 is already gated on useXform so HSJ correctly bypasses it. Per-score cost unchanged (0.030 s median, 40 x 60-tip HSJ TreeLength, both builds). HSJ scores on mixed data CHANGE and generally DECREASE, the old value having included spurious inapplicable mismatches; NEWS and both vignettes say so. STILL OPEN on this row: the XFORM MPT-set residue (pool membership selected at R/MaximizeParsimony.R:1618 on search-time scores at differing rootings), which is XFORM-only and untouched here.
Migrated 2026-08-04 from dev/red-team/findings.md; pre-tracker ID T-374. References to T-374 in source comments, dev/red-team/log.md, commit messages and PR bodies mean this issue — see dev/red-team/migration-map.tsv. Cross-repo references use the fully-qualified form agent-issues/TreeSearch#<n>; a bare #n means this repo.
Severity: P1 · Area: 10 (HSJ/XFORM kernels)
Location:
src/ts_hsj.cpp:140-161(alpha·d/mterm),src/ts_sankoff.cpp:74-85(min-over-root-states),R/recode_hierarchy.R:176; trigger sitets_collapse_poolinsrc/ts_rcpp.cppOne fixed unrooted 10-tip topology scored under each of its 10 tip-rootings: EW
28ten times (invariant); HSJ α=1 →24.5/25; HSJ α=0 →20ten times. So the dependence is entirely thealpha·d/msecondary term —fitch_label_char's DELTRAN-style directional uppass resolution, whose comment atts_hsj.cpp:77-91justifies invariance to level ordering only and is silent on rooting. Across 300 random topologies, XFORM is rooting-dependent on 165/300 and HSJ on 197/300. The wrong-answer symptom (seed 20260728): reported best15.5, butTreeLength()of the 6 returned MPTs gives16 15.5 15.5 16 16 16. Causation pinned by three controls, which localise the trigger more precisely than the original finding did: (i)collapse = FALSE→ all six give15.5, and pairwise RF shows both runs return the same six unrooted topologies (every row and column of the cross-RF matrix has a 0) — only the rooting differs; (ii) it is per-tree, not cross-tree — the topology that returns16scores15.5under all 7 of its other edge-rootings, so the single rootingts_collapse_poolimposes is uniquely pessimal for it; (iii) scorer mismatch is excluded —R/tree_length.R:183-189andR/MaximizeParsimony.R:1220-1222use the same.NonHierarchyWeights,.HSJAbsentState,.HierarchyToBlocksand alpha. Falsified code assertion: the comment abovets_collapse_poolinsrc/ts_rcpp.cppclaiming the tip-0 re-root is "what makes the contraction rooting-invariant" is false for HSJ/XFORM. Why P1 regardless of design intent: a reported best score that cannot be reproduced from the returned tree is a wrong user-visible result, and 4 of 6 trees returned in one MPT set do not share a score under a common rooting. DESIGN QUESTION SETTLED FOR HSJ, 2026-07-28, FROM THE PAPER — the rooting-dependence is a BUG, not an intended rooted objective. Hopkins & St John (2021) define the score as a minimum over internal-node labelings of a sum of dissimilarities over branches (p.3: "for a fixed phylogenetic tree, we extend the character labelings of the leaves to the internal nodes of the tree and compute the minimal score"; p.6: the algorithm keeps "both the possible score when the controlling primary character is present and when it is absent" and "return[s] the minimal score"). The HSJ dissimilarity is symmetric in its two endpoints (d= "the number of nonmatching secondary characters", p.5), the branch set of an unrooted tree is rooting-independent, and a minimum over labelings introduces no orientation — therefore the objective is rooting-invariant by construction. Corollary for the fix: the paper prescribes a two-state DP carrying both present/absent possibilities and minimising;fitch_label_char's single directional DELTRAN-style uppass commits to one resolution, which is neither the paper's algorithm nor guaranteed minimal, and is the source of the α-term dependence measured here. So the fix is to implement the paper's DP (or equivalently resolve on marginal MPR sets), not to pin a rooting. XFORM HALF NOW DECIDED, 2026-07-29 — decision document:dev/plans/2026-07-29-t374b-xform-rooting-policy.md(branchclaude/t374b-xform-rooting-policy; evidencedev/red-team/heavy-tests/xform-rooting-oracle.R, pure R, no build, exits 0 while its predictions hold). The earlier reading — "an asymmetric step-matrix character is intrinsically rooted, so pin a rooting and stopsankoff_forced_rootbeing-1, which needs TBR rethought" — is superseded. Recommendation: keep the objective unrooted, do NOT pin a rooting; fix the reporting discrepancy instead. Three results drive it, and the lead evidence is (2), not (1). (1) The tempting cheap fix —forced_root_state = 0("absent at the root") — is rejected as INCOHERENT ON ITS OWN, not as harmful. It does not restore rooting-invariance: the pinned criterion's value varies across root positions on 84–117/120 random 9-tip topologies, spread up to 5. Read that carefully — pinning the root state defines a different, explicitly rooted criterion which the oracle confirms is perfectly well-defined at any fixed rooting, so its spread is the price of leaving its root position arbitrary and is not commensurable with the free-root arm as "worse". (An earlier draft of this row over-claimed a refutation on exactly that bad comparison; likewise thenSec = 0pinned cell, 84/120, is a tautology — constraining the root's label on a symmetric matrix must make the root position matter — and is a valid asymmetry control for the FREE arm only.) What it does establish: shipping the one-liner into today's pipeline, which leaves the root position unchosen and moves it at four sites, makes the score depend on a root the user never selected by up to 5 steps. So pinning the state is only meaningful together with pinning the position — i.e. it is Option 2, not a one-liner. (2) The dependence is bounded and one-directional — this is what actually licenses the recommendation: the asymmetric part of the cost matrix is a gradient (f(absent)=0,f(present)=nSec/2), sototal = Σ_edges s − Σ_internal f − f(root) + Σ_tips f; the root is chargedftwice, giving a tested spread bound ofnSecper block (an earlier tighternSec/2guess was falsified by the oracle — rooting subdivides an edge, soΣ_edges sis not itself rooting-invariant). An arbitrary rooting always overstates the well-defined min-over-rootings objective; 87–98% of rootings attain it, mean overstatement 0.02–0.17 steps — so the search optimises a sound but loose upper bound, not a wrong number. Fully ambiguous tips (-1) are the aggravating factor (~3× the dependence rate, 8× the overstatement); present-unknown (-2) is not. (3) TBR is NOT the obstacle — the "rethinking TBR" framing conflates two rerootings. TBR's fragment reroot is a genuine unrooted-topology change and is compatible with a pinned root; the default!phys_rerootpath never physically reroots the whole tree (ts_tbr.cpp:2967-2984enumerates root-edge moves instead). Whole-tree rerooting happens at just four enumerable sites:ts_tbr.cpp:2997(legacy,TS_PHYS_REROOTonly),ts_fuse.cpp:20(called:344/:381/:398),ts_rcpp.cpp:2088(ts_collapse_pool, the T-374 trigger),ts_sector.cpp:1101. The min-over-root-states atts_sankoff.cpp:74-87is therefore not sufficient (it frees the root's state, not its position); the correct unrooted objective is min over rootings and states, affordable on the report path only ((2n−3)×the Sankoff term). Recommended fix = makeMaximizeParsimony's reported score andTreeLength()agree on one rooting (prefer min-over-rootings on the returned pool, which also makes the MPT set self-consistent), and document XFORM as rooting-sensitive with thenSec-per-block bound. Do not require rooted input or error out — XFORM has no root to demand. A/B verdict: do NOT run the rooting-pinned A/B (the only cheap pinned variant is the incoherent one from (1), so a score difference could not distinguish "pinning helps" from "that arm drew a favourable root"; the effect is second-order behind T-377, whose Fitch-only candidate screen would confound it; and accepted scores are already authoritative viafull_rescoreat a rooting that is stable within a TBR pass — incoherence enters at the four sites above, not in TBR's accept loop). Two further false root-invariance comments to annotate on fix:ts_tbr.cpp:123-124andts_tbr.cpp:2999, alongside the already-recordedts_collapse_poolone (ts_tbr.cpp:802-818is correctly root-dependence-aware and needs no change). The gating blocker is the already-filed T-377 (TBR's candidate scan is hierarchy-blind): the Sankoff term is absent from XFORM's TBR candidate screen, which is first-order and must be settled before any rooting measurement is interpretable. T-374b re-derived that independently from code and briefly mis-filed it as a new T-383 before finding T-377; the duplicate row is deleted and T-377 now carries the three refinements (thehas_na-conditional convergence sweep, the over-permissivedominatedtest, and this A/B gating). A paper-derived fix-gate is committed atdev/red-team/heavy-tests/hsj-paper-oracle.R. Not reproduced, and not load-bearing: the XFORMMaximizeParsimonysymptom (0/6 seeds — mechanism confirmed, symptom did not surface as those searches returned one tree each; the finder claimed this for HSJ, not XFORM) and the all-hierarchy7.5vs8case (analogue gave 3/3). A third consequence worth carrying: because the objective wanders with the rooting during search, TBR's own accept/reject comparisons may be incoherent across moves — unquantified; a matched A/B against a rooting-pinned variant would settle it. Verified REAL by opus verifier at pinned1a94403b. XFORM half REPRODUCED against tipa8fbba84on 2026-07-31 (T-385), and the decision document's Q5 is INCOMPLETE — read this before implementing it. Reprodev/red-team/heavy-tests/t385-xform-report-agreement.R(36 tips, 6 blocks,nSec = 2, seed 1 / search seed 11,maxReplicates = 4): reportedattr(res, "score") = 178;TreeLength()of all 32 returned trees= 183(gap 5); the same topology under 8 rootings spans 178-183 (spread 5, within theSum nSec = 12bound); and the 32 "equally parsimonious" trees carry 2 distinct scores (183, 182) at a common tip-1 rooting. The reported number is not wrong — 178 is attained by 2 of the 8 rootings; the engine records a score at one rooting (R/MaximizeParsimony.R:1672,score = result$best_score) and returns a topology re-rooted elsewhere byts_collapse_pool(:1600-1630), whose:1604comment asserts the very root-invariance this finding refutes. RETRACTION of this row's first refinement (same day, before any fix was written): I claimed the MPT-set symptom survives a canonicalised report path, on a measurement of "2 distinct scores (183, 182) among 32 MPTs at a common tip-1 rooting". That measurement was invalid and the claim is withdrawn. It rooted each tree at its owntip.label[1], which afterRenumber()is a different taxon for different trees -- so it was a different rooting per tree, not a common one. Re-measured properly (dev/red-team/heavy-tests/t385-diagnose-rooting.R): all 32 returned trees are already rooted at the kernel's tip 0 (atTip132/32, root degree 2) and at a genuinely common rooting all score 183 -- 32 distinct topologies, 0 disagreement, score range 183-183. So the returned pool is self-consistent, and the MPT-set symptom this row records from the earlier session ("4 of 6 trees do not share a score") did not reproduce here; it is not evidence against canonicalising, and pool re-filtering is not required. What survives, and is the P1: the reported score (178) is not the score of the tree at the rooting it is actually returned at (183) --R/MaximizeParsimony.R:1672reportsresult$best_scorefrom search state whilets_collapse_poolreturns every tree at the tip-0 rooting. Also real: the same topology spans 178-183 across 8 rootings, i.e. the objective is genuinely rooting-sensitive within theSum nSec = 12bound (178 is attained by 2 of the 8, so the reported number is a valid value at some rooting, just not the returned one). XFORM reporting half FIXED (T-385), and the MPT-set residue is now CONFIRMED REACHABLE and no longer silent. Fix onclaude/t385-xform-report-agreement(stacked on PR ms609#277):TreeLength()canonicalises the rooting in BOTH its single-tree and multiPhylo methods (the latter previously rooted only trees that arrived unrooted, so an already-rooted tree kept its own rooting), andMaximizeParsimony()rescores the returned pool through that same path before reporting. Acceptance script goes gap 5 -> 0 and rooting spread 5 -> 0. The residue:MaximizeParsimony()now warns when the returned trees do not share the canonical length, and that warning fired immediately on an existing test --test-ts-xform.R's "Xform search handles all-hierarchy data (zero Fitch words)" returns a pool spanning 7 to 9. So this row's original "4 of 6 trees do not share a score" IS real and reachable (all-hierarchy matrices), even though my own 36-tip re-measurement of it was invalid (retracted above). Cause is unchanged and still open: pool membership is selected atR/MaximizeParsimony.R:1618on search-time scores taken at differing rootings. Fixing it means canonicalising pool selection, which movesn_topologiessemantics and interacts withcollapse-- unanalysed by the decision document, deliberately out of T-385's scope. HSJ reporting deliberately NOT canonicalised at the REPORTING boundary: there rooting-invariance is required by the method, so canonicalising the report would convert a wrong objective into a stably-wrong one and burn this row's HSJ half. HSJ HALF FIXED AND MERGED 2026-08-02 as34eea581(PR ms609#281,feature/hsj-rooting-invariant-dp, branched off93c81a9a). CI on the merge: sense-check and both Linux matrix jobs green;windows-latestfails at "Set up R dependencies (covr)", which fails identically on the base branch and is unrelated. FIRST, A CORRECTION THIS ROW ITSELF PROPAGATED, AND WHICH COST A ROUND OF WORK: the claim thatfitch_label_char's directional pick is 'neither the paper's algorithm nor guaranteed minimal' conflated two different things, and the prescription 'implement the paper's two-state DP' was already satisfied.score_hierarchy_block()'sa(n)/p(n)recurrences ARE Algorithm 1 (p.15) lines 6-7, term for term; the two-state DP was implemented and was ALREADY rooting-invariant, because its branch costs are symmetric and it minimises over the root's own state. The defect was never in the DP -- it was thatd(u,v)was read off the SECONDARY labelling. Nor would copying Algorithm 1 literally have helped: its line 2 setsL(n)to the first-pass Fitch labelling, i.e. the DOWNPASS, which is root-dependent, and line 8 updatesL(n)in postorder; Algorithm 1 takes "Tree, T, with root r" as INPUT, so Theorem 2's claim that it returns the minimal score cannot hold as stated -- if it did, its output would be rooting-invariant, and the measurements below show it is not. THE MEASUREMENT THAT COLLAPSED THE DESIGN SPACE. Under ANY most-parsimonious reconstruction of secondary j, the number of branches on which j changes equalsFitchLen_j. So for an ALL-PRESENT block the alpha term is(alpha/m) * sum_j FitchLen_jEXACTLY, whichever labelling the uppass picks -- verified 50/50 on random data, and it reproduces Fig. 1 exactly (6+alpha/3+2alpha, i.e. the paper's 7 and 5 at alpha = 1). The rooting-dependence therefore lived ENTIRELY in blocks with MIXED present/absent primaries, measured over every edge-rooting of 30 random 9-tip topologies at alpha = 1: all-present 0/30 at93c81a9aalready; mixed 21/30 (m=2) and 26/30 (m=4), spread 1.00;?-primary 6/30 and 18/30, spread 0.50. Note the pre-existing figures in this row (165-197/300, "24.5/25 across 10 tip-rootings") were taken at1a94403band are superseded; the oracle's "2 pass / 3 fail" is likewise stale, PR ms609#280 took it to 8/0. TWO MECHANISMS, BOTH FIXED. (1) Semantic, and the dominant one."-"was admitted as an ordinary state of a secondary character. Where a controlling primary codes the structure absent its secondaries do not exist, so"-"there is not a state the character takes; admitting it let the uppass propagate"-"INWARDS and label a node in the middle of the PRESENT region inapplicable, where it is disjoint from every present neighbour in every secondary at once and the branch was chargedd = m, the full alpha. That over-charge is wrong under ANY rooting (the paper'sdcounts nonmatching secondaries among characters that APPLY, p.5), so this is a correctness fix that removes most of the rooting spread as a side effect, not a rooting fix. Note the comment formerly atts_hsj.cpp:45-47asserted this defect as deliberate design. Secondaries are now wildcarded at tips whose PRIMARY token can mean absent -- keyed off the primary, not off the secondary's own token, because a"-"secondary at an unambiguously-present tip is contradictory data thatValidateHierarchyrejects upstream. Alone this took mixed to 3/30 and 7/30 (spread 0.50/0.25) and?-primary to 1/30 and 3/30. (2) Directional. The remaining resolution was a DELTRAN uppass whose direction, and tie-break support counts whose SUBTREES, were properties of the input rooting. The labelling passes now run over a traversal rooted canonically at tip 0, built once per block; neighbour lists are sorted by node index so the traversal cannot inherit the incoming orientation, and the kernel's own root becomes an ordinary degree-2 node that Fitch passes through transparently. Result: 0/30 in every regime, spread 0.0000. THE HEADLINE SYMPTOM IS CLOSED. Twelve HSJ searches on 16-tip mixed matrices: 3/12 returned trees whoseTreeLength()disagreed with the reported score, worst gap 0.75, one pool spanning 31.25/31.5/31.75/32 -- at93c81a9aAND after mechanism (1) alone. Now 0/12, every pool internally consistent. So the semantic fix alone did NOT close this row; the canonical rooting was needed. WHY CANONICAL ROOTING, AND WHY IT IS NOT PR ms609#278 AGAIN. The exact minimum over labelings is the inapplicable-Fitch problem (min over primary labellings of boundary-count +(alpha/m) * sum_j FitchLen_jover the present forest) and is not available in linear time; the per-branch min-over-MPR relaxation is REFUTED (a 0,1,0,1 caterpillar givessum of min= 0 againstmin of sum= 2). A composite Sankoff over{absent} + all secondary combinationswith the symmetric HSJ cost matrix IS exact and IS fully rooting-invariant -- the matrix satisfies the triangle inequality, so even edge-subdivision is safe -- but it is exponential in nSec and is what the XFORM path already does, so it would delete HSJ's reason to exist alongside XFORM (O(nk)). The maintainer was given these three options with the measured residual attached and chose the canonical rooting. It is distinct from PR ms609#278 in kind: that canonicalised at the REPORTING boundary, leaving search on a root-dependent objective; this sits INSIDE the kernel, so the objective the SEARCH optimises is a function of the unrooted topology andMaximizeParsimonyagrees withTreeLengthby construction, with no rescore. It buys INVARIANCE, NOT EXACTNESS -- do not record this row as "HSJ now computes the paper's minimum". TESTS, six intest-ts-hsj.R, verified against a build of93c81a9ain a throwaway worktree (usetestthat::set_max_fails(Inf)-- the default cap of 10 silently hid the last test on the first attempt): rooting invariance on a fixed mixed block 2 failures (alpha = 0 passes, as the control locating the dependence in the alpha term), on 8 random matrices 6,"-"-vs-"?"5, MaximizeParsimony pool 5. Two are regression FLOORS that pass either side (Fig. 1 = 7/5; the all-present closed form). Two had to be rebuilt before they tested anything: the p.5<= 1 per branchbound is far too loose summed over a tree and PASSED pre-fix, replaced by the sharper invariant that"-"and"?"at an inapplicable secondary must score alike; and the MaximizeParsimony test wasmaxSeconds-bounded and flaky in BOTH directions, nowmaxReplicates-bounded on a construction where 14 of the first 40 seeds are discordant pre-fix.hsj-paper-oracle.Rnow discriminates: 12 pass / 1 fail at93c81a9a(only the new mixed-block check [3b] fails, 9/12, worst spread 0.667) against 13 / 0 after, with every other check identical across the two builds. Its rooting check now sweeps all2n-3edges rather than n tip-rootings. COLLATERAL: the four false root-invariance comments are narrowed to XFORM rather than deleted (ts_tbr.cppreroot_at_tip contract and reroot-cycle note,ts_rcpp.cppts_collapse_pool,R/MaximizeParsimony.Rcollapse note);ts_tbr.cpp:802-818was already correct and is untouched; the XFORM rescore atMaximizeParsimony.R:1709is already gated onuseXformso HSJ correctly bypasses it. Per-score cost unchanged (0.030 s median, 40 x 60-tip HSJTreeLength, both builds). HSJ scores on mixed data CHANGE and generally DECREASE, the old value having included spurious inapplicable mismatches; NEWS and both vignettes say so. STILL OPEN on this row: the XFORM MPT-set residue (pool membership selected atR/MaximizeParsimony.R:1618on search-time scores at differing rootings), which is XFORM-only and untouched here.Migrated 2026-08-04 from
dev/red-team/findings.md; pre-tracker ID T-374. References toT-374in source comments,dev/red-team/log.md, commit messages and PR bodies mean this issue — seedev/red-team/migration-map.tsv. Cross-repo references use the fully-qualified formagent-issues/TreeSearch#<n>; a bare#nmeans this repo.