Skip to content

C++ search - #210

Draft
ms609 wants to merge 1161 commits into
mainfrom
cpp-search
Draft

C++ search#210
ms609 wants to merge 1161 commits into
mainfrom
cpp-search

Conversation

@ms609

@ms609 ms609 commented Mar 19, 2026

Copy link
Copy Markdown
Owner
  • other optimizations + features

Manual testing underway; shiny app in particular has some usability issues.

@ms609
ms609 marked this pull request as draft March 25, 2026 14:21
ms609 added a commit that referenced this pull request Mar 28, 2026
ms609 added a commit that referenced this pull request May 18, 2026
In R CMD check, R runs as a non-interactive subprocess with captured
stdout. R_FlushConsole() calls fflush() on that pipe; when the buffer
fills the call blocks indefinitely, causing the 6 h GHA timeout seen
on every ubuntu runner for PR #210.

Gate the \r-overwrite progress line and the flush behind R_Interactive
(FALSE in batch/check contexts). Interactive sessions are unchanged.
At verbosity >= 2 in batch mode, emit plain \n-terminated lines so
diagnostic logs still carry progress detail without the flush risk.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ms609 added a commit that referenced this pull request May 18, 2026
In R CMD check, R runs as a non-interactive subprocess with captured
stdout. R_FlushConsole() calls fflush() on that pipe; when the buffer
fills the call blocks indefinitely, causing the 6 h GHA timeout seen
on every ubuntu runner for PR #210.

Gate the \r-overwrite progress line and the flush behind R_Interactive
(FALSE in batch/check contexts). Interactive sessions are unchanged.
At verbosity >= 2 in batch mode, emit plain \n-terminated lines so
diagnostic logs still carry progress detail without the flush risk.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ms609 and others added 22 commits July 20, 2026 07:01
The Goloboff-1996 union-construct screen refutation: lower-bound proof note (unsound with collapsed uppass finals) and the gate script that exercises it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Kernel speed-baseline / reslice-plan / goloboff-gates notes plus the disc_nstates and corpus_state_heterogeneity characterisation scripts (+ small .rds caches). The TS_AUDIT_PROBE bail-depth instrumentation from the same original commit is intentionally OMITTED here: origin/cpp-search rewrote ts_fitch.cpp (+264 lines, spr-drift/L3b) so the probe's call-sites no longer map; it remains preserved on branch claude/mission-b-kernel-goloboff-6f5c0e for a future manual port if needed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…#2/#3) + harnesses

project5432 Mission A: all cold-start-basin-capture angles refuted by measurement.
- #1 constructor CLOSED five ways: per-split detectability (anti-detect), absolute
  synapomorphy count (chance), joint clique recovery (36th pctile, count-matched),
  scaffold-sufficiency (all-deep-fixed 1/12), clique-START routability (0/60, worse
  than RAS), clique-proportional reweight (~uniform, wrong-skew).
- #2 basin-hop schedule CLOSED (all near-optimal TS trees >=97 TBR from 1943).
- #3 structured breadth CLOSED (random resample lands farther, 0/400).
Capstone: the 1943 deep backbone is an emergent aggregate (~4/183 clean-supporting
chars), so no per-char/compatibility/reweight start-generator can bias toward it;
reach is search-side only (not a hard limit -- TNT ~1/3). Adds dev/benchmarks harnesses.
Updates the deliverable's open-angles + status so the next 1939 phase does not re-run these.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e skips them

The mission brief still listed constructor/schedule/breadth as 'none yet refuted' — the
exact re-investigation trap. Now points to the closure record + capstone (emergent-aggregate
backbone => search-side only, not a constructor).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…t-in)

Recovered from a stale GitHub Desktop auto-stash. POOL_RESEED
(TS_POOL_RESEED) reseeds a fraction of replicates from a retained
best-score pool tree instead of a fresh Wagner start, giving the
diverse equal-length pool its own sectorial re-solve pass. Terminal
fuse (TS_TERMINAL_FUSE=1) runs a final fuse over a few best-score
anchors after the loop converges, recombining complementary clades
the in-loop fuse never reached. Both default-off; deployed behaviour
is byte-identical. Also carries dev/profiling driver scripts used
during that exploration and a stray citation-syntax fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…result

Commit the score-verified best-known artifacts for the Mission-A epitome
instance project5432, so they can never go missing again (the lesson of the
retracted "1939" trace phantom):

- dev/benchmarks/hardtail/project5432_best_1942_tnt.tre  (1942, verified)
- dev/benchmarks/hardtail/project5432_ts_reach_1943.tre  (1943, verified)
- dev/benchmarks/hardtail/README.md  (regime, provenance, config, re-run footnote)

Result (Hamilton array 17943743, 16 seeds x ~48h): TreeSearch's aggressive
perturbation swarm reaches 1943 (6/16 seeds) — the new best TS-generated score,
one step below the prior TS-side best of 1944. Recorded observationally:
block-1 cold still floors ~1945-1947; sustained iteration under the perturbation
stack descends to 1943.

Reframing: 1943 is a SHARED TS/TNT floor (28h heavy TNT also floored at 1943
across all seeds; 1942 = one lucky regen; 1939 = phantom). The residual
1943->1942 gap is a rarity/luck-and-speed gap, not a TS pathology. Updates the
Mission-A plan doc's dated result-note + artifact pointers; CLOSED sections
left intact. Swarm ran on Hamilton lib TreeSearch 2.0.0 (lags cpp-search);
a current-engine re-run to settle whether TS reaches 1942 is held, not launched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two of four compute_collapsed_flags* recompute sites in tbr_search
(post-accept :2859, legacy-reroot :2991) called the conservative
kernel unconditionally instead of branching on collapse_aggr like the
other two sites (:1539, :2967). Under TS_COLLAPSE_AGGRESSIVE=1 this
meant the aggressive criterion only applied to the first pass before
silently reverting for the rest of the search.

Confirmed via the strategy briefing that no "aggressive once, then
conservative" design was intended -- this was an oversight. Exact-
preserving (collapsed[] only gates enumeration skips, never a
scorer); verified via load_all smoke test that TS_COLLAPSE_AGGRESSIVE=1
still reaches the same full-rescore-matched score, and
test-ts-collapsed.R passes unchanged (24/24).
Areas 1 (Fitch scoring correctness) and 2 (Search topology invariants),
both directed at what is new in MaximizeParsimony since the 2026-05-26
inspection. Both seams ran dry at opus on the new default-ON surface
(L3b incremental edge-sets, bail-cutoff drop-the-+1, x4 reroot, spr_search
default) with airtight construction/oracle evidence; each escalates to fable
next visit, aimed at the named oracle-blind residuals (compute_insertion_edge_sets
absolute correctness; update_base value correctness).

Area 2 filed T-334 (P3): the aggressive-collapse neighbourhood criterion
reverts to conservative after the first accepted move (exact-preserving,
opt-in default-OFF prototype). It was independently adjudicated unintentional
and fixed in a concurrent commit (b1e9e09) on this shared branch, so this
commit carries only the red-team coordination log plus the area-1 inline
doc fix -- NOT the ts_tbr.cpp code fix.

- dev/red-team/log.md: area-1 & area-2 round entries; last_focus -> 2
- src/ts_data.cpp: correct stale "TS_PACK_LOCAL (default OFF)" header comment
  (default-ON since 2026-07-16) -- A1-OP-01, trivial doc-only

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Area 3 (Ratchet & perturbation), directed at what is new since the
2026-05-26 inspection: sector in-sector drift, RSS size-scaling,
TS_SECT_COLREDUCE, prune-reinsert exact scorer, fuse per-round reroot.
Opus fresh-surface pass, pinned @ ec8edd5.

Core deliverable: the full-dataset-rescore firewall = YES -- every
sector/prune-reinsert splice-back accept is gated on score_tree against the
COMPLETE dataset (HSJ/XFORM sectors skipped entirely), so the HSJ/XFORM-blind
reduced dataset and TS_SECT_COLREDUCE are heuristic-quality (reach), never a
wrong returned score. The exact_verify NA-cache reach-leak hypothesis traced
to a confirmed non-bug (per-instance cache, isolated rd.data, dataset+regime
key). Perturbation composition, fuse per-round reroot, and prune-reinsert
exact scorer all clean.

Filed T-335 (P3): TS_SECT_COLREDUCE leaves per-pattern arrays stale -- inert
under the current EW-only gate, a live wrong-score + OOB tripwire if the gate
is loosened toward IW (filed defensively; concurrent mission-b work is on
ts_sector). RT-A3-02 refuted (finder inverted the field-divergence claim).

Seam ran dry -> next visit escalates to fable, but the residual is thin;
flagged area 3 as a downtier/retire candidate for the next meta-review.

- dev/red-team/findings.md: T-335
- dev/red-team/log.md: area-3 round entry; last_focus -> 3

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Area 4 (Parallelism & RNG), opus fresh-surface pass on the ~2.5k
lines of mission-b speed code new since the 2026-06-15 inspection.

- T-336 (P1): shared ConstraintData raced across parallel Resample()
  workers (worker lambda captures cd by ref; parallel_driven copies
  per-worker, resample path does not). Public-API reachable. Fix filed,
  NOT applied (concurrency fix to concurrently-edited ts_parallel.cpp).
- T-337 (P2): build_dataset Rf_error longjmps across threads on the
  parallel resample worker path for n_states>32 (T-309 class).
- T-338 (P3): 3 consolidated inert worker-thread tripwires
  (getenv / verbosity-Rprintf / TS_AUDIT_PROBE statics).

Crown-jewel RNG class confirmed intact (T-309 fix holds; POOL_RESEED
cleared). Seam still yielding -> next area-4 visit stays at opus.
Verified: opus finder + opus peer verifier (P1) + haiku (low/latent).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… race)

parallel_resample's worker lambda captured the single ConstraintData
`cd` by reference and handed the SAME pointer to every worker's
resample_search -> driven_search. ConstraintData carries mutable
per-tree/per-clip workspace (constraint_node, dfs_entry/exit,
clip_zones, clip_tip_mask, posthoc_data) that each search WRITES via
map_constraint_nodes/impose_constraint and reads back to gate
acceptance. Sharing one instance across workers is a data race (UB),
gates a constraint against another worker's tree mapping, and breaks
reproducibility. Reachable from public
Resample(constraint=, nReplicates>1, nThreads>1).

worker_thread (parallel_driven) already makes a per-worker copy
`cd_local = *ctx.cd_prototype`; parallel_resample did not. Fix mirrors
that pattern: each worker copies the active cd once (searches within a
worker run sequentially, so one copy per worker suffices) and passes
its own pointer; null/inactive cd passes through unchanged.

Red-team area 4 (Parallelism & RNG), 2026-07-24: opus finder + opus
peer verifier traced all five links; found+fixed same turn. Build
clean (build-fast 6.6s); functional smoke (constrained + unconstrained
parallel Resample) passes. Thread-race validation belongs in GHA-ASan/
TSan (local ASan blocked on MinGW).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The P1 shared-ConstraintData data race in parallel_resample was
approved by the user and fixed same-turn (f951df5). Update the
findings row to FIXED (kept until merged) and correct the area-4
log entry (was "filed, not applied").

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Area 5 (Data pipeline & simplification), opus fresh-surface pass on
the post-2026-06-15 commits: per-block-local-alphabet packing
(89ebd87, default-ON), char homoplasy-ordering (d8c5999), ambiguity
fixes (53ffb05/942bfe5c). ts_ls.cpp excluded as a behaviour-preserving
extraction (78b7414) of already-inspected LS code.

Seam ran DRY at opus. The P1-capable "corrupted ds -> silently wrong
score on every rescore path" hypothesis was tested directly (from-tip
on/off differential) and did NOT reproduce: TS_PACK_LOCAL default-ON is
byte-identical to OFF across NA/EW/IW/profile/XPIWE. char-reorder is
index-based (T-335 shape N/A); per-block DAT-001 ruled out;
DAT-001/DAT-002 fixes hold.

One finding, PACK-COLRED-01 (reduce_sector_columns_ew leaves the new
plane_state field zero-init) verified REAL-but-inert (haiku) and FOLDED
into T-335 (same function, same EW-gate inertness) rather than a new
row.

Next area-5 visit escalates to fable; thin residual = PACK_LOCAL x
HSJ/XFORM tip-data interaction (set by the Rcpp bridge after
build_dataset). last_focus -> 5; next rotation area = 6.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…339..T-344

Ran opus and fable finders in parallel on identical fresh area-6 scope
(R<->C++ interface) per explicit user request, both to make progress and
to check whether fable regresses toward opus-like guardrail caution on
adversarial-framed bug-hunting. No regression observed: fable was as
direct and technical as opus, found a non-overlapping headline bug, and
closed a cross-check opus's report had left open. Six findings verified
and filed (T-339/T-340 P2, T-341/T-342/T-343/T-344 P3); one trivial doc
fix applied inline (timings roxygen list, both finders flagged the same
gap independently). last_focus: 5 -> 6.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`tree = <multiPhylo>` used only its first tree and silently discarded the
rest, so resuming a search from a previous run's most-parsimonious trees
threw away exactly the topological diversity that tree fusing consumes.
Replicate i now warm-starts from tree i; replicates beyond the pool build
random Wagner trees as before.

R side keeps the whole list and normalizes each member (resolve, drop
extraneous tips, renumber, reroot so the root's first child is a tip),
resolving the tip-set mismatch once up front because that step mutates
`dataset`.  C++ `start_edge` becomes `start_edges`, and both the serial
loop and the parallel worker select on `rep < start_edges.size()` instead
of `rep == 0`; reps are claimed dynamically in parallel but rep -> tree
stays 1:1, as rep -> seed already does.  `unpack_runtime()` accepts either
a bare edge matrix (the flat `ts_driven_search()` wrapper and `Resample()`
still pass one) or a list.

Verified free for existing callers: against a build of a485581 with the
same seed, cold start, a single `phylo`, and a `multiPhylo` of one are
bit-identical in score, `replicate_scores`, `candidates_evaluated` and
returned topologies.  `rep < 1` is `rep == 0`, so this is structural.

Also fixes a segfault this path could reach.  `ape::unroot()` accepts
TreeTools' `order = "preorder"` attribute and then mishandles it, so
unrooting a TreeTools tree yields an edge matrix containing NA; rooting
one killed the session below the level at which R can trap anything.
Pre-existing (a485581 crashes identically) but newly reachable from pool
members at index >= 2, which normalization no longer skips.
`.CheckStartTree()` rejects such an object before any traversal code sees
it, and again after `MakeTreeBinary()`, which can emit one itself.

Unused pool members are now reported against the replicates actually run:
consumption is bounded by whichever stopping rule fires first, usually
`targetHits`, so `maxReplicates` was not a truthful bound to warn against.

Known gap, left alone deliberately: some genuinely valid unrooted trees
fail with "argument is of length zero" (also pre-existing).  Rooting
before the binary check fixes it but changes which arbitrary resolution
`MakeTreeBinary()` picks for other unrooted inputs (star tree 208 -> 191),
so it needs an A/B rather than a blind patch.

Findings T-345..T-353 from a directed red-team round on this diff.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…-353)

Diff-scoped review rather than an area visit, so `last_focus:` stays at 6
and the organic rotation still goes to area 7 next.

Yield: two P1s (one segfault reachable from a single `phylo`, pre-existing;
the same defect newly reachable from pool members at index >= 2), one P2
(pool consumption bounded by `targetHits`, not `maxReplicates`, so the
warning and docs were both wrong), and six P3s.  All fixed in ee91dac
except the valid-unrooted-tree gap, which needs an A/B because rooting
first changes the resolution `MakeTreeBinary()` picks.

Nothing found at P1 in the C++ index logic: `rep` bounds, `start_n_edge`
sharing and `init_from_edge` were confirmed sound rather than left unsaid.

Records the rung/version rule that a dry verdict is scoped to the model
version that produced it, plus the escalation backlog it feeds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…licates

`adaptiveStart = TRUE` updated the Thompson-sampling bandit for every
replicate, including those handed a starting tree via `tree = `.  Such a
replicate never reaches the strategy switch in run_single_replicate()
(`if (starting_tree)` short-circuits it), so the default WAGNER_RANDOM was
credited for a Wagner start that was never built.  Reseeded (POOL_RESEED)
reps were already excluded on exactly this reasoning; `user_started` now
extends the same guard to user-supplied starts.

Latent for replicate 0 alone until `tree = <multiPhylo>` (ee91dac) began
warm-starting one replicate per supplied tree, turning one phantom credit
into k per search.

decay() moves outside the guard: it measures how stale the accumulated
evidence is, not which arm ran, so it still fires when a warm-started
replicate improves the best score. Kept as a separate `if` so the A/B
below measures one change, not two.

dev/benchmarks/ab_bandit_warmstart.R (3 datasets x 5 seeds, matched
starting pools, serial): mechanism cleanly confirmed -- wag_rand's share
of bandit attempts drops from 60.3% to 25.9% (uniform would be 25%), and
the clearest cell (Agnarsson2004 seed 2) shows the bandit locking onto
wag_rand for all 15 cold replicates before the fix, exploring after. No
time-to-optimum regression: score ties 13/15 (1-1 split on the rest), wall
trends slightly in the fix's favour in both replications. Verdict: ADOPT.

dev/benchmarks/ab_bandit_coldstart_invariance.R backs the NEWS claim that
`adaptiveStart` used without `tree = ` is unaffected: every cell across 2
datasets x 3 seeds matches before/after on both score and the per-arm
attempt vector exactly, as it must -- with no warm start, user_started is
always false and the guard is unchanged.

Reviewed under the same red-team round as ee91dac (T-345..T-353): the
finder found no defect in this hunk; the P1/P2 findings were both in the
warm-start-pool change it landed alongside.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…heck

A valid unrooted binary tree has nrow(edge) == 2 * NTip - 3, which fails
the "is this already bifurcating?" test the same way a genuine polytomy
would, since that test conflates "needs resolving" with "needs rooting".
MakeTreeBinary() then misreads the unrooted root's legitimate degree-3
trifurcation as a polytomy, corrupting the tree (dropping its `order`
attribute), which previously surfaced downstream as "argument is of
length zero" in Preorder/RenumberTips.

A/B'd on Vinther2008/DeAssis2011/Wortley2006 (8 seeds x RandomTree(root=
FALSE) and StarTree starts, which already worked pre-fix) to confirm no
reached-score regression before adopting.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fixes MaximizeParsimony(tree = <unrooted phylo>) failing with "argument
is of length zero" on valid unrooted starting trees. See commit 6fa9776
for root cause and A/B verification.

# Conflicts:
#	NEWS.md
…el-identity check

Fable's per-token rate is ~2x opus's, so near-identical token counts this
round mean it actually cost ~2x, consistent with doctrine -- not contrary
to it as the first version of this note claimed (user caught the error).
Also records the post-round self-identification check on both finder
agents (distinct, correctly-matched model IDs; not dispositive, since a
self-report only reflects the harness's own template, not server-side
attestation -- Console billing is the authoritative source).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Area 7 at sonnet (Sonnet 5); last visit yielded 5, so same rung with a fresh
agent, and the 4.6->5 bump agrees with that routing rather than competing.

A date-filtered `git log --since=2026-06-16 -- inst/Parsimony/` decided the
round's shape: the only non-test source change since the last visit is the
MorphyLib removal's 9-line footprint, so server.R and mod_search.R are
byte-identical to what that round already mined, and the never-opened modules
were the whole new surface.

Yield: one P2 and six P3s from nine candidates (one refuted in verification,
three self-refuted by the finder with evidence).  T-354 is the substantive one
and was confirmed by a committed artifact rather than by tracing: a passing
snapshot of the "R script" download reconstructs the plotted trees by stride
sampling where the app ran WideSample(), so the downloaded script recomputes
silhouettes and cluster consensus from a different, non-representative subset
(19/48 overlap on Sun2018; the stride subset contains duplicate topologies).
Fires on the default path for any tree set over 48 trees.

Two pieces of tracker rot found and fixed:

- findings.md listed T-309..T-313 as open; all five were fixed in 78b7414 on
  2026-06-19 and nothing flipped their status.  Each confirmed at tip by
  inspection, annotated pending a tidy archive.  The stale rows cost tokens
  this round -- the finder brief asserted they were open.  A tidy pass is due.

- focus-areas.md row 7 named a phantom server/events.R while omitting
  global.R, ui.R, app_state.R and logging.R.  Three of this round's findings
  sit in files the old row did not name, so the stale scope was suppressing
  yield.

Seam still yielding; next area-7 visit stays at sonnet and should take
mod_search.R in full, the one genuinely uncovered block left.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ms609 and others added 30 commits August 4, 2026 18:01
TreeState::init_from_edge derives n_tip, n_internal and n_node from the
edge count alone, which identifies a tree only when it is binary.  A
multifurcating edge list broke that derivation in both parities: an odd
edge count left the topology arrays one element short, so parent[] and
left[]/right[] were written out of bounds, while an even one kept the
indices in range but rooted the tree on a real tip, leaving a one-element
postorder whose downpass read prelim.data() - total_words.  Either way
the caller got a plausible number instead of an error, and the number
varied between identical calls; a polytomous startEdge segfaulted.

Refuse the edge list at that boundary rather than at each of the R entry
points that reach it: edge_list_is_binary() checks the shape from the
edge arrays alone, before anything is written, and init_from_edge throws
for Rcpp to forward.  build_topology_tree() clones the same derivation
for the least-squares path and gets the same check.  ts_driven_search()
screens start trees with the predicate on the main thread, since an
uncaught throw on a parallel worker would terminate the session.

TreeLength.list(), .CheckTreeCharLen(), TreeScore() and EdgeListScore()
gain the R-level check so the message matches the one TreeLength.phylo()
has always given.  FastCharacterLength() is left unchecked, as
documented; the kernel now gives it the same message.  The Shiny app
scores every tree it displays, so it now searches with collapse = FALSE.

Also extends the T-261 zeroing audit in reset_states() to name the
collapse kernels, whose whole-row memcmps read words no pass writes, and
to name every path that zero-fills the state arrays rather than only
init_from_edge.  The T-382 one-sidedness comment keeps its original
reasoning for prelim -- a tip sibling always carries real states, so a
ratchet-zeroed block makes equality harder -- and gains the down2 /
subtree_actives case, where the words really are always zero.

Fixes #16
Fixes #24

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ps tests

External review of the prior commit found the roxygen text factually
wrong: AdditionTree() never returns a $score (it discards result$score),
so "affects only the reported $score" is false -- correct it to state
plainly that numeric concavity has no effect on AdditionTree()'s output.

Also addresses two test gaps the review flagged: the first new test
exercised ts_wagner_tree() directly rather than AdditionTree() (retitled
to make that explicit, and its equality assertion tightened to a
directional/finiteness check so a NaN or sign error couldn't pass), and
two cheap regression tests were missing: that concavity = Inf still
forwards min_steps = integer(0), and that concavity = "profile" still
runs cleanly now min_steps is computed post-recoding.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
`Resample(constraint =, nReplicates > 1)` errored: `R/Resample.R:372` splats
`.PrepareConstraint()`'s whole list into `ts_parallel_resample()`, which has no
`consZero` formal.  It was the one splat site of four not filtered, and no test
covered a constrained `Resample()` at all; a test now asserts the filter against
every flat kernel's formals.

The collapse protection over-reached.  It protected the MRCA of a group
unconditionally, so an unsupported branch that merely happened to be the
narrowest node containing the 0 group was returned resolved -- the "unsupported
non-constraint branches still collapse" half of the promise, and a shift in
`n_topologies`.  It now protects only when no realising edge survives the
contraction on its own.

A discarded replicate no longer feeds the strategy bandit, the Chao1 coverage
scores or the replicate report: a violating tree scores better than any legal
one, so those would be credited to whatever produced it.  Its stopping rules
still run -- skipping them would outlive the deadline and swallow an interrupt.
An interrupted replicate is now tested but not repaired, since
`impose_constraint()` has no interrupt check of its own.

The constraint check short-circuits on the locked-node mapping, which is cheaper
than the post-hoc Fitch check and strictly stronger, so only an unmapped split
pays for the latter.  Constrained wall-clock on Vinther2008 (30 paired seeds)
goes from a 1.12 median ratio, 11 seeds >10% slower, to 1.003 with none >10%.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ci: fix the Windows/macOS MaxMin resolution failure that reddened every branch
test(EasyTrees): let the dataset load settle before setting treeRange
Self-review of the two commits before it.

A constraint is enforced as a split throughout -- the locked-node filter screens
rearrangements on it, and impose_constraint() repairs to it and nothing else --
so a character with a third state has taxa nothing constrains.  The capture gate
was judging that same tree by the stricter full-Fitch reading, which is not a
standard the search can reach: a probe on a three-state constraint discarded all
four replicates and errored with an empty pool, where a partial answer existed.
The gate now follows the mapping, and `.PrepareConstraint()` warns at input that
an intermediate state is unconstrained, rather than leaving the caller to infer
from `@param constraint` that it is not.

`constraint_satisfied()` refreshes the DFS timestamps alongside the node ids.
map_constraint_nodes() alone left the two out of step, and spr_search() reads
both without re-mapping, so the `sprFirst = TRUE` warmup could classify a
regraft against this tree's nodes and another tree's timestamps.

A start whose repair fails falls back to a Wagner build, which is exactly the
constructor that can exhaust its reshuffles and return a violating tree; it is
now repaired rather than trusted.

Also: the collapse protection's group sizes are counted once instead of per
tree; the kernel-formals test now asserts that no constraint field is dropped,
not just that none is unknown; `.ConstraintViolated()` indexes nodes by column
so its accumulation runs down a column-major matrix rather than across it.

Constrained wall-clock on Vinther2008 (20 paired seeds) is 0.95 median against
`cpp-search`, scores identical.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
red-team: area 12 round 2026-08-04 — scope-coverage diff, routing-channel finding, rearrange.cpp header correction
fitch_label_char()'s uppass loop formed `&co.kids[co.kidOff[node]]`
before testing `nk`, the node's child count.  CanonOrder stores children
CSR-style, so `kidOff[n]` for a childless node is whatever `kids.size()`
happened to be when the DFS popped it -- and for the LAST node popped
that is the final size, every other node having already contributed its
children by then.  `co` arrives as a const reference, so this is
`std::vector<int>::operator[](size()) const`: a dereference of one past
the end.  The downpass and the tie-break accumulation loops above both
already `continue` on `nk == 0`; this loop did not.

Reproduced against `-D_GLIBCXX_ASSERTIONS` (flag in PKG_CPPFLAGS, since
~/.R/Makevars.win zeroes PKG_CXXFLAGS; 34 hits in the build log).
Pre-fix, four test files abort on entry to their first HSJ block with
`Assertion '__n < this->size()' failed`; post-fix all four run clean:

  test-tree_length.R             abort ->  77 passed
  test-ts-xform.R                abort -> 138 passed
  test-ts-hsj.R                  abort -> 147 passed
  test-ts-resample-hierarchy.R   abort ->  74 passed

The reporter's second reproducer (test-ts-xform.R) is the same defect,
not a second one: it aborts inside the third test, which is the file's
first `inapplicable = "hsj"` search -- hence exactly five assertions
first, from the two preceding pure-xform tests.

`kid` is never dereferenced when `nk == 0`, so no value was read through
the bad reference and no score moves: 900 HSJ and x-transformation
lengths over random matrices (4-16 tips, alpha 0/0.5/1), the issue's own
data, and three seeded end-to-end searches are bit-identical either side
of the fix, Inf entries included.

Fixes #51

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… routing and scope gaps

The user-level `/red-team` skill carried dual-mode machinery for a `findings.md`
store this project deleted at the 2026-08-04 migration. Retired to
`~/.claude/skills-retired/red-team/` — the only surviving record of file mode, which
is still correct for a project with no GitHub remote — and replaced with a
project-local skill that assumes issues throughout, alongside `/next-issue`.

The one-off process for standing up a repo under `agent-issues` (fork, default
branch, push block, labels, Actions, secrets, findings migration) moves to a
subsidiary `github-repo-setup.md` that a normal round never opens.

Fixes #41 — the rotation's routing state was tier-only:
- a work-shape gate between brief assembly and dispatch, so a recorded "the next
  visit should NOT be a finder" verdict stops the round and reports instead of
  auto-dispatching. Areas 10 and 13 both carry one, and area 13 is next.
- `needs-escalation` is documented as a tier flag and nothing else; sequencing and
  cross-area residuals stay prose, and step 3 now reads `escalation-backlog.md`
  whether or not the label query hit — a hit used to suppress the row holding the
  actual ask.
- the finder brief carries the area's open issues, so a finder cannot spend its
  budget re-investigating a mechanism already filed.
- `area:N` semantics stated: it marks which area owns the code, not which round
  found it, and the compound label query ANDs correctly.

Addresses #42 — scope coverage:
- the `tidy` pass now leads with a scope-coverage diff (glob `R/*.[Rr]`; the
  lowercase `pp_info_extra_step.r` is invisible to a case-sensitive pattern).
- area 14 (statistics & support metrics, 14 files) and area 15 (legacy pure-R
  search API, 9 files) added, both `sonnet` / UNMEASURED. #42 recommended `opus`
  for 14 and review-once-then-park for 15; the maintainer chose measured maturity
  for both. N is now 15.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ion state only

The previous commit put the skill and its repo-setup guide inside this repo. Both
were wrong. The skill is shared across projects, so burying it here takes
/red-team away from every other project; and `github-repo-setup.md` documents
standing a repo UP under agent-issues, which is precisely the thing already done
here and never needed again — it belongs where a not-yet-configured project reads
it, next to the skill.

Both now live at `~/.claude/skills/red-team/`, with the #41 routing fixes applied
there. This branch keeps only what is genuinely project state: areas 14 and 15,
N = 15, and the README/AGENTS label range.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Issue agent-issues#51 asked for this: the memory note it was
filed against points at the ASan workflow as the route to a container-OOB,
and that workflow was red on trunk for the very defect the previous commit
fixes.  A local -D_GLIBCXX_ASSERTIONS build reproduces the same class in
seconds on Windows and can be aimed at one test file, so it belongs in the
subsystem's own memory file, next to the flat-vector layouts that make this
the recurring failure mode here.

Also records the two ways to misread its output: the abort names the
container type, never the call site; and `lib.loc` must be an absolute
Windows path, or test_file()'s chdir breaks the lazy-load DB and fakes
several regressions.

Qualifies the in-source issue reference per AGENTS.md, since src/
fast-forwards to the public upstream, where a bare `#51` resolves to an
unrelated issue.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Code-review follow-up on the T-300 rerooting patch.

`n_reroot_accepts` exists solely so the new regression test can tell a
correct dirty-set rerooting rescore from an unexercised one.  It was
incremented for every rerooting move regardless of `incremental_ok`, so
under HSJ/XFORM — where a rerooting accept still falls through to
`full_rescore` — the counter would be positive while the arm it guards
never ran.  Adding an HSJ/XFORM case to the test would then have made
`expect_gt(n_reroot, 0)` pass vacuously, which is exactly the failure the
counter was added to prevent.  Gate it on `incremental_ok`.

Also record what it actually counts: the increment sits at the rescore,
before the accept/reject decision, so a move counted here can still be
rejected by the constraint check, the tabu test or the score comparison.
It is an upper bound on accepted rerootings, not a count of them.

No behaviour change: EW / IW / NA / NA-IW all have `incremental_ok`, so
every case the test drives is unaffected.

Third hunk is a stale comment: the NA dirty passes are no longer
SPR-only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Independent review of ca5c1c0 disputed "the last node DFS popped".  It
is not one node but a run of them: every childless node reached after the
final push_back carries the end offset.  Measured on an R mirror of
build_canon_order() over 900 random trees, 2-24 tips:

  trees with NO kidOff==size node: 0
  trees with >1 such node        : 843
  kidOff/kidNum CSR consistency  : OK

The first line matters most -- the pre-fix code formed a reference to
co.kids.end() on EVERY HSJ scoring call, not on some unlucky shape.  The
third rules out the alternative reading that the guard papers over a
corrupt CSR: kids[off + 1 .. off + num] is exactly each node's canonical
children, so kidOff/kidNum are sound and only the missing nk == 0 test
was wrong.

Comment and NEWS reworded accordingly; the script lands under
dev/red-team/reviews/ as the standing evidence.

The same review found a SEPARATE unguarded bound -- tip_labels' row count
is validated at neither Rcpp bridge, giving an identical `_Tp = int`
const-operator[] abort from a hand-crafted TreeSearch::: call.  Confirmed
against a build already carrying this fix, so it is not the same defect,
and it is unreachable from the public API.  Filed as
agent-issues#58 rather than widened into this branch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…opped

Review read "which is what made the AddressSanitizer workflow unusable" as
crediting ASan's own instrumentation, and objected -- correctly -- that ASan
watches accesses, not address arithmetic, and that co.kids.reserve(n_node)
leaves the offending address inside the live allocation anyway.

The claim was about the right defect but named the wrong instrument.  What
aborted the gcc-ASAN job is visible in agent-issues#51's own
evidence: `stl_vector.h:1282 ... Assertion '__n < this->size()' failed`, a
libstdc++ precondition check, not a sanitizer report.  The r-hub gcc-asan
container compiles with the hardened library; the flag is not in ASan.yml,
which is why grepping .github/workflows/ for it finds nothing and misleads.

Reworded to say what actually fires and why that workflow could not get
past this package.  Also distinct from the EARLIER ASan unusability that
PRs #262/#275 fixed -- that was the runner configuration; this is a defect
the now-working workflow immediately hit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
docs: state Wagner's equal-weights placement contract; pass min_steps
fix: reject non-binary trees at the TreeState boundary
…103cc

perf(tbr): finish T-300 — incremental rescore for TBR-rerooting accepts
…/ on Windows

core.ignorecase=true on Windows folds "/.agent*" onto the tracked .AGENTS/
memory directory (case-insensitive prefix + wildcard suffix match), causing
`git add .AGENTS/memory/*.md` to silently refuse without -f. Narrow the
pattern to the actual .agent-<id> naming convention and add a defensive
negation for good measure.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…lision

Narrow /.agent* gitignore pattern to stop matching .AGENTS/ on Windows
fix: OOB read in HSJ token_states/CanonOrder lookup
fix: enforce user constraints at the start-tree, pool-capture and collapse boundaries
…-4853c6

red-team: add focus areas 14 and 15 (N=15); skill rewrite lands user-level
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants