C++ search - #210
Draft
ms609 wants to merge 1082 commits into
Draft
Conversation
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
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 <[email protected]>
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 <[email protected]>
This was referenced May 19, 2026
…31/T-332 work # Conflicts: # src/ts_collapsed.cpp
…ecord Keeps the reusable rssPicks SearchControl knob (exposes existing SectorParams::rss_picks_per_round; default 0 = byte-identical) and the mission-gate harness+FINDINGS documenting the sectorial-escape thread's closure (mechanism resolved, wall-lever ruled out). See dev/benchmarks/missiongate/FINDINGS.md. Co-Authored-By: Claude Opus 4.8 <[email protected]>
test-ViewChars.R and test-Distribution.R were silently broken since the app was modularised: they drove consensus inputs by pre-modularisation unqualified ids (keepNTips, consP, outgroup, concordance ...), so every consensus interaction no-oped and the committed baselines had drifted stale (still citing the long-removed MorphyLib refs). Nothing caught this because these tests are not run in CI. Also fixed mapLines, which is a top-level input (ui.R) that had been wrongly namespaced treespace-. - Namespace every input to its real id (consensus-*, mapLines top-level); regenerate and certify the ViewChars/Distribution value baselines (verified the app now reaches each intended state). - Port the legacy tests/shinytest/*.R download-content coverage: add savePlotZip script snapshots via expect_download() at the same states, with a shared normalize_download() transform (setup.R) that is CRLF-safe and scrubs dates/versions/system lines. Teeth verified: a meaningful script change is detected; a volatile version change is scrubbed. - SearchLog: the saveZip session log embeds non-portable search results (trees[[N]], allTrees[1:M], nThreads); assert deterministic log-generation markers instead of a brittle full-content snapshot, and check saveNwk/saveNex structurally. This ports v1 intent in a CI-portable form. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
The three legacy shinytest scripts (Distribution.R, SearchLog.R, ViewChars.R) plus their -expected snapshots and the shinytest.R runner are now fully superseded by their shinytest2 AppDriver counterparts under tests/testthat/ (interaction coverage, plus the download-content coverage ported in the previous commit). The v1 runner could no longer execute anyway: shinytest (v1) was dropped from DESCRIPTION Suggests some time ago (only shinytest2 remains), so `library(shinytest)` errored on the first line. codemeta.json still lists shinytest; that file is generated (see .github/workflows/codemeta.yml) and will drop the entry on its next regen from DESCRIPTION, so it is intentionally left untouched here. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Prepares the three integration tests to run under GitHub Actions.
- setup.R gains a shared new_app_driver() factory and a wait_stable() retry
wrapper. wait_stable() re-attempts app$wait_for_idle(), which can transiently
error ("An error occurred while waiting for Shiny to be stable") right after a
heavy dataset load; a first-try success incurs no delay.
- Disable expect_values() browser screenshots everywhere
(expect_values_screenshot_args = FALSE) and drop the committed .png baselines.
The screenshot is flaky even run-to-run on one machine (anti-aliasing / render
timing) and carried no reliable signal; the value (json) + download-content
snapshots carry the coverage.
- Route value snapshots through a thin expect_vals() wrapper (a seam for a
future normalising transform) and add the .md download-filename baselines that
the previous commit missed (they live at the _snaps root, not the per-test
subdir).
Note on portability: expect_values() hashes the app's rendered plot output, so
the value snapshots are only portable on the OS that generated them. The shiny
CI job therefore runs on Windows (the development platform) rather than Linux,
which keeps CI output consistent with the committed baselines.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Restructure R-CMD-check.yml from a single 6-config matrix into fail-fast waves,
mirroring ../MaxMin:
sense-check (ubuntu-24.04-arm, release) -- cheapest leg, NO concurrency group
so a rapid follow-up push never cancels it; every commit gets a build signal.
core (needs sense-check) -- Windows (+codecov), R 4.1 (highs pin), R devel;
per-leg concurrency cancels superseded in-flight legs.
full (needs core) -- macOS x2 backstop; the ~10x-runner-minute legs only run
once core is green (in practice, about to merge).
All six original check legs are preserved, just reorganised into waves. There is
deliberately no workflow-level concurrency (see header comment).
New: a `shiny` job running the EasyTrees shinytest2 suite, gated on BOTH the
sense-check passing AND the app having changed (a `changes` job with
dorny/paths-filter feeds its `if:`, since workflow-level `paths` cannot combine
with `needs:`). It runs on windows-latest -- the development platform -- because
expect_values() hashes the app's rendered plot output, so the value snapshots
are only portable on the OS that generated the baselines. It is
`continue-on-error: true` (NON-BLOCKING) until its stability on CI is
established, and uploads any *.new.* snapshot diffs as an artifact for triage.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
EasyTrees shinytest2 repair/port/CI (local) + sectorial mission-gate (origin); disjoint file sets, automatic merge. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
… + Δ-probe expand_and_reinsert (T-266, 2026-03-27) used the union-of-finals fitch_indirect_length_bounded approximation that the June directional fix (#26/#27) replaced everywhere else — it was MISSED. Port it to the exact edge_set scorer, mirroring ts_wagner.cpp:487: - per-tip compute_insertion_edge_sets (prelim is current via wagner_incremental_rescore) + fitch_indirect_length_cached per edge. Also a gated, non-perturbing -DTS_SCOREAPPROX_PROBE oracle that tallied Δ = exact_cost(E_bounded) − min_E exact_cost(E) per placement. On Zanol (forced pruneReinsertCycles): bounded chose strictly-worse edges in ~62% of placements, mean ~6 steps, ~48% greedy-regret SHARE. After the port the probe reports Δ=0 at every placement (production == exact argmin). NOTE: this is greedy-regret SHARE, not realizable wall-clock — prune_reinsert auto-enables ONLY at nTip>=120 (`large` preset); NO mission dataset reaches 120t (max=88). So this path runs on zero default mission searches. Land + time-matched A/B (NNI-polish `large` preset, ≥120t) is COMPOSITION-gated (#40). Worktree-only; NOT for cpp-search until composition. Tests: prune-reinsert 44/0, drift 22/0, ratchet 17/0, tbr 28/0. Co-Authored-By: Claude Opus 4.8 <[email protected]>
…-search data race Post-review hardening of the exact-directional scorer port. The -DTS_SCOREAPPROX_PROBE diagnostic block's function-local `static` counters (sa_placements/sa_delta_*/sa_*_exact_sum) are mutated with no synchronization, but expand_and_reinsert runs concurrently on parallel-search workers — an unsynchronised data race (adversarial review, CONFIRMED low). thread_local gives each worker its own tally (per-thread partials under multithreading; exact for the single-threaded diagnostic runs this probe is intended for). Production (probe undefined) is byte-identical and unaffected. Co-Authored-By: Claude Opus 4.8 <[email protected]>
…w coverage gap) Adversarial review of the exact-directional scorer port found no prior test drove expand_and_reinsert on NA (inapplicable) or IW (implied-weights) data, nor asserted score self-consistency in any regime — yet prune_reinsert_search runs on both (its guard only early-returns for PROFILE/HSJ/XFORM). The port is a ranking-only construction heuristic; the accepted tree is gated by a regime-correct score_tree() (strict-improve + revert), so the REPORTED best_score must equal an independent length recompute of the RETURNED tree in every regime. Adds two tests (Vinther2008, 23t, forced pruneReinsertCycles=3): NA-EW and IW (concavity=3) — each asserts result$best_score == ts_fitch_score(returned tree). Both green. Empirically confirms the port cannot corrupt the accepted tree on NA/IW (mirrors the ts_wagner.cpp all-regime convention, not the ts_tbr.cpp guard). Co-Authored-By: Claude Opus 4.8 <[email protected]>
…sert (T-266) Lands 41b0d2373's exact edge-set insertion scorer in expand_and_reinsert (replacing the union-of-finals fitch_indirect_length_bounded approximation; mirrors the June directional fix at ts_wagner.cpp), plus branch post-review hardening: 4007f8e port (byte-identical to 41b0d2373); 0fb6de9 thread_local probe accumulators (parallel-search data-race fix, production byte-identical); 98122e5 NA + IW score-soundness regression tests (Vinther2008). The 'NOT for cpp-search until composition (#40)' note in 4007f8e's body is superseded: landing the correctness fix is decoupled from the #40 time-matched A/B. Dormant path (prune-reinsert auto-enables only at nTip>=120 / large preset; no default mission search reaches 120t) so nil current wall-clock risk. Verified: clean consistent rebuild + targeted tests 123 pass / 0 fail (incl new NA/IW soundness + wagner-quality). Co-Authored-By: Claude Opus 4.8 <[email protected]>
Drop the non-portable value snapshots from the ViewChars/Distribution AppDriver tests. app$expect_values() embeds rendered-plot output (a data-URI image hash plus float plot geometry) that differs machine to machine even on the same OS, so it could never green on GHA. Replace each expect_values() with wait_stable() (retrying wait_for_idle wrapper) and keep the portable coverage: the savePlotZip download-script snapshots (which encode app state), the interaction path itself, and SearchLog's log markers. Regenerate all download baselines; delete every _snaps/*/*.json. Rewrite test-app-smoke to load a bundled dataset (Sun2018) before asserting "N trees in memory" -- a fresh boot has data-dataSource="file" with nothing loaded, so the old assertion was checking a state the app never reaches. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
The testServer module tests drifted from the app code while the suite was CI-unwired: * test-mod-search.R carried a hand-copied SearchConfidenceText frozen at an older signature; global.R's real one grew stopReason/replicateScores (consensus/timeout stop notes + Chao1 coverage note). The module now passes those args, so every observeEvent(input$implied.weights) that calls DisplayTreeScores() errored with "unused arguments" and aborted before completing the T-165 run-stats reset -- cascading into the reset/cancel-file assertions (656-658, 519). Re-sync the stub verbatim to global.R and flag it KEEP-IN-SYNC. * TreeLength()/ScoreSpectrum() are TreeSearch exports but the file only attached shiny, so they were "could not find function". Attach TreeSearch. * The consensus stop-note assertion pinned "consensus stable"; the shipped text is "consensus tree unchanged across recent replicates". Align the assertion to the real output. (See note to maintainer: confirm this wording is intended, not an accidental UX regression.) * test-mod-data.R expected tipLabels() to be NULL with no trees, but mod_data.R deliberately guards `return(character(0L))`. Align the expectation. Verified: test-mod-search PASS=66 FAIL=0, test-mod-data PASS=13 FAIL=0. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…ARNs - global.R: consensus stop-note now reads "Search stopped: consensus stable across recent replicates." (terse, descriptive); test-mod-search stub kept in sync + assertion pinned to "consensus stable". - test-mod-clustering.R: library(shinyjs) so the clThresh observer resolves runjs() (was 3 "could not find function" WARNs). - test-mod-consensus.R: suppressWarnings around two isolated-harness artefacts (transient non-numeric signif / length-zero if) irrelevant to the assertions. CI fails on WARN; whole EasyTrees suite now FAIL 0 | WARN 0 | PASS 192. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* fix(constraint): reject impossible constraints, guard collapsed-split OOB (T-329) An impossible (non-laminar) constraint reached random_constrained_tree() unfiltered: .PrepareConstraint() only filtered splits by size, never checked pairwise compatibility. In random_constrained_tree(), a split that loses all its tips to tighter non-laminar splits collapses (split_root = -1); if it still has a strict-superset parent, the parent's child-split loop pushed that -1 as a phantom item, causing tree.parent[-1] and tree.left/right[n_tip-1] out-of-bounds writes. Primary fix: .PrepareConstraint() now validates pairwise split compatibility (disjoint or nested) and stops with a clear error before reaching the C++ builder. Defensive fix: guard split_root[j] >= 0 in random_constrained_tree()'s child-split collection, mirroring the existing root-level guard. Co-Authored-By: Claude Sonnet 5 <[email protected]> * ci(ASan): switch mem-check runner to ubuntu-latest (x86) The r-hub gcc-asan container image has no arm64 variant; on ubuntu-24.04-arm the container dies at init before any R work runs (every run since 2026-06-19 failed in ~30-40s). Cherry-picked from TreeSearch main's fix (PR #262) so the T-329 fix can be verified under a working ASan CI. * ci(asan): install phangorn for tests leg, exclude MaxMin from vignettes leg The tests leg only installs hard deps + Config/Needs/memcheck (Suggests are skipped for tests/examples), but tests/testthat/*.R make heavy unguarded use of phangorn::phyDat() etc. -> "there is no package called 'phangorn'". Add it to Config/Needs/memcheck so it installs regardless of leg. The vignettes leg installs all immediate Suggests via a single pak::pkg_install() batch, which resolves plain package names against CRAN/Bioconductor only - it doesn't see the local DESCRIPTION's Remotes mapping. MaxMin is GitHub-only (Remotes: ms609/MaxMin), so that batched solve fails outright with "Can't find package called MaxMin", taking every other Suggest down with it. Exclude MaxMin, mirroring the existing Rogue exclusion: its only use (WideSample()) is requireNamespace-guarded and no vignette/example exercises it. * fix: guard zero-length memcpy in TreeState::load_tip_states Datasets whose characters are all uninformative (e.g. every observed state is a singleton) collapse to zero blocks and a zero-length tip_states vector in build_dataset(). std::vector::data() on an empty vector may legally return nullptr, which memcpy's nonnull-parameter attribute forbids — flagged by UBSan (gcc-ASAN run 28662381835) as a null-pointer violation reached via ts_char_steps() -> init_from_edge() -> load_tip_states(). Skip the copy when there is nothing to copy. Covered by the existing "ConcordantInformation() works" regression test in test-Concordance.R, which already exercises an all-singleton dataset via ConcordantInformation() -> CharacterLength() -> ts_char_steps(). Co-Authored-By: Claude Sonnet 5 <[email protected]> * fix(constraint): use four-gamete split compatibility, not laminar-subset (T-329 follow-up) T-329 (19f4f97) added a laminarity gate to .PrepareConstraint() that tested each constraint character's "1"-side as a full bipartition vs everything-else and accepted only disjoint-or-nested pairs. This rejected SATISFIABLE constraints: it (1) folded wildcard "?" and unconstrained tips into the "0" side though they are free to plot either side, and (2) tested only three of the four split-compatibility cases, missing the case where the two "0" sides are disjoint (the "1" sides jointly cover the constrained tips). The gcc-ASAN CI (run 28662381835) surfaced this on the pre-existing Morphy test and the tree-search vignette "complex-constraints" chunk (ab|cef & abcd|ef, g free) -- both displayable on ((a,b),(d,(c,(e,f))))+g. T-329's own "impossible" example {t1,t2,t3}&{t3,t4,t5} was likewise satisfiable (coexist on ((t1,t2),t3,(t4,t5))). Fix: build a companion 0-group matrix and apply the four-gamete test on both groups (wildcards excluded) -- incompatible iff all four group intersections are non-empty. Valid constraints return to their working pre-T-329 path; genuinely-impossible ones still error. The C++ OOB guard (ts_wagner.cpp split_root[j] >= 0) is the real crash fix and is retained unchanged. Tests: corrected the first T-329 case to a genuinely four-gamete-incompatible constraint ({t1,t2,t3}&{t2,t3,t4}); added a positive regression asserting the non-laminar-but-compatible case is accepted. Verified locally: Morphy suite, vignette chunk, and 446 constraint tests all green. Co-Authored-By: Claude Opus 4.8 <[email protected]> * fix(wagner): guard zero-Fitch-words start-tree build (all-hierarchy HSJ/xform) When every character belongs to a hierarchy block, the recoded equal-weights dataset has zero blocks, so DataSet::total_words == 0 and the per-word Fitch state vectors (ds.tip_states, edge_set) are empty. wagner_tree() unconditionally took the address of element 0 of those empty vectors (&ds.tip_states[tip * total_words] == &ds.tip_states[0], and likewise &edge_set[node * tw]) to feed the incremental insertion-cost machinery. That is undefined behaviour (address of a past-the-end element of an empty vector) and aborted under the hardened libstdc++ assertions in the gcc-ASAN CI (run 28662381835, ts-xform group; _GLIBCXX_ASSERTIONS operator[] __n < size()). With no Fitch characters every insertion cost is identically zero, so guard the per-word work behind have_words = ds.total_words > 0: skip the edge-set precompute and evaluate the indirect length as 0, while still running the DFS so constraints are honoured and the first legal edge is chosen. The search then optimises the tree from the Sankoff (xform) / hierarchy-DP (HSJ) term alone. The have_words == true path is behaviourally identical to before. Regression tests (test-ts-xform.R, test-ts-hsj.R) run an all-hierarchy dataset through MaximizeParsimony() so the hardened/ASAN CI covers the zero-words path. Verified locally with a -D_GLIBCXX_ASSERTIONS -O0 -g build: the whole ts-xform group (incl. SK-01, which previously aborted) and ts-hsj group now pass with no abort; a normal equal-weights search is unaffected. Co-Authored-By: Claude Opus 4.8 <[email protected]> --------- Co-authored-by: Claude Sonnet 5 <[email protected]>
…harness Investigates whether the T-327 guard `build_postorder().size()==n_internal` (impose_one_pass, src/ts_constraint.cpp:735) is equivalent to structural validity, and whether a net-zero corruption can slip it to reach the advertised std::bad_alloc from MaximizeParsimony(constraint=). Adds dev/red-team/heavy-tests/impose_validity/ — a standalone C++ harness (no R/Rcpp/Morphy/SIMD) that enumerates all (2n-3)!! rooted binary trees for n_tip=4..8, runs the verbatim kernel functions (topology_spr, collect_edges_*, find_maximal_subtrees, compute_node_tips extracted from HEAD at build time) + the real build_postorder, and models the whole impose_constraint loop. Findings (T-333, P3): the guard is NOT a full validator — it admits reachable net-zero TYPE-1 left/right corruption (double-ref + orphan) that DOES reach the DFS helpers, so its comment is inaccurate. But the hypothesised P1 is REFUTED: 0 root-reachable left/right cycles AND 0 parent[] cycles across ~1.33M accepted-invalid trees (no std::bad_alloc on either vector, incl. the pre-verify tbr_search->reroot_at_tip path), and 0 corrupt final trees survive the callers' map_constraint_nodes verify-and-discard (no wrong answer). Fix = replace the size-check with a full left/right arborescence check (O(n)). Files: findings.md + to-do.md (T-333), log.md (area-13 round entry). Co-Authored-By: Claude Opus 4.8 <[email protected]>
…tor (T-333) The T-327 backstop in impose_one_pass's try_move — postorder.size() == n_internal (commit 6b60f23) — is not equivalent to structural validity. topology_spr's root-child degenerate case can emit a net-zero corruption (one internal node double-referenced +1, one orphaned -1) that lands on exactly n_internal and slips a size check, then feeds a malformed tree to the next reanchor DFS and to reroot_at_tip's parent walk. (The hypothesised std::bad_alloc P1 was refuted — no reachable cycle on either vector — and no malformed tree was ever returned; correctness rested entirely on the callers' verify-and-discard. This makes the guard self-sufficient.) Add structurally_valid() (O(n): child slots in range + left!=right; in-degree 1 for every non-root node / 0 for root; a root DFS visiting every node exactly once; parent[] the exact inverse of left/right). try_move now validates before rebuilding the postorder and reverts on any corruption. Performance: the check is confined to impose_one_pass/try_move, which runs ONLY when constraint= is passed to MaximizeParsimony. TreeState::build_postorder (97 hot-path call sites) is untouched and keeps its > n_internal cap as the bad_alloc backstop, so unconstrained search pays nothing; within a constrained run it is the same O(n) complexity class as the build_postorder already there. Verification: - Exhaustive harness (dev/red-team/heavy-tests/impose_validity/) re-pointed at the real, verbatim-extracted structurally_valid: probe() now asserts it equals the independent full_validity() oracle on every tree, with a g_guard_mismatch gate (exit 3 if they ever diverge — the durable CI signal against reverting to a size-only check). Result: 0 disagreements across all (2n-3)!! rooted binary trees n=4..8 (every net-zero witness the old guard admitted is now rejected). - R integration test added (test-ts-impose-constraint.R "T-333: structural guard survives root-child repair, honours constraint") — the stricter guard neither crashes nor over-rejects valid repairs through the linked engine. - Package builds clean (-Wall -pedantic); full test file green. findings.md row removed (resolved); to-do.md T-333 -> FIXED (pending CI); log.md area-13 round annotated. GHA CI (ASan + full check) pending on push. Co-Authored-By: Claude Opus 4.8 <[email protected]>
fitch_na_score()'s first-uppass tip loop iterates every tip 0..n_tip-1 and dereferences tree.final_[tree.parent[tip] * total_words + ...]. That assumes every tip is attached, but a partial tree can legitimately be scored while some tips are detached: expand_and_reinsert() (prune-reinsert perturbation, T-266) maps the optimised reduced backbone into the full node space and calls score_tree() BEFORE the dropped tips are Wagner-reinserted. Those dropped tips still carry the parent == -1 sentinel from init_wagner_state(), so anc == -1 and the index becomes (-1) * total_words == -total_words, a wild negative subscript (size_t-wrapped). That is out-of-bounds on tree.final_ / tree.prelim. Only the NA path trips this: the EW path (fitch_uppass) and every other pass in fitch_na_score (Pass 1, Pass 2 internals, Pass 3) walk tree.postorder, which contains only attached nodes, so they already ignore detached tips. The NA first-uppass tip loop was the lone place reading tree.parent[] for all tips unconditionally. It aborted under the hardened libstdc++ assertions in the gcc-ASAN CI (run 28681669404, ts-prune-reinsert group; _GLIBCXX_ASSERTIONS operator[] __n < size(), long unsigned int == uint64_t on x86-64). Under a normal build the same read silently touches out-of-bounds heap. Fix: guard the tip loop with `if (anc < 0) continue;`, making the NA uppass honour the same attached-nodes-only contract the EW path already follows. The guard is inert for a complete tree (every tip's parent is >= n_tip), so a normal search is behaviourally identical. Detached tips get their prelim/final recomputed by wagner_incremental_rescore() when they are reinserted, and the accept-gate score_tree() runs on the completed tree, so scoring is unaffected. Regression coverage: the NA and IW score-self-consistency tests in test-ts-prune-reinsert.R (both drive prune-reinsert on the inapplicable Vinther2008 dataset; IW reuses it via fitch_score_ew -> fitch_na_score) exercise this exact path and previously aborted before their assertions ran. Verified locally with a -D_GLIBCXX_ASSERTIONS -O0 -g build: test-ts-prune-reinsert.R now completes 52/52 with no abort (was exit 134), and the ts-na-* / ts-driven groups still pass with no assertion. Co-Authored-By: Claude Opus 4.8 <[email protected]> (cherry picked from commit 638a6ce)
…env vars" This reverts commit 5a2af27.
…hine's speed The coverage wave ran for the first time and immediately failed on my own parallel stopPatience test: FAIL 1 / PASS 11627 under `covr::codecov()`, against FAIL 0 / PASS 11628 in the same commit's ordinary check. So the test was not wrong about the code -- it was wrong about the environment. Mechanism, and it is the one I documented in the very commit that added the test (998891e): on the parallel path the dry spell is counted over replicates completed into the shared pool and evaluated only when the coordinating thread polls, every 200 ms. covr instrumentation makes each replicate far slower, so far fewer land per poll interval and `stopPatience = 3` fires genuinely earlier in the search -- 13.063 against 12.988. `expect_equal(short score, full score)` was therefore asserting a coincidence of timing, not a property of the rule. Stopping sooner is *allowed* to cost score; that is the entire trade-off `stopPatience` exists to offer, and it is what the shipped preset measurements quantify. Asserting equality contradicted the feature's own documentation. What the rule does guarantee is already asserted and untouched: the search stops before `maxReplicates`, and `perturb_stop` is set. The score check is now only that it is finite and not grossly worse, which holds at any speed. Third time this session a test of mine encoded an environment assumption -- after `as.Splits`/`%in%` dispatch and the ARM64 ordering chase. The pattern: an assertion that passes because of how fast, how attached, or how ordered the local environment happens to be, rather than because of what the code promises. Co-Authored-By: Claude Opus 5 <[email protected]>
…-374 Coordination-only (to-do.md + findings.md). No code. Three P1s (T-374, T-375, T-376) were sitting in findings.md with NO to-do.md row, and T-385 existed only in agent memory -- in neither file. The queue therefore read as "one open P3" (T-324) while four P1 correctness defects were live, which is also why the standing tasks were computing as P1 under to-do.md's own <3-open-tasks rule. T-385 is filed as the implementation ticket for the ALREADY-DECIDED T-374b policy, not as a new finding: T-374's row already records both of its symptoms, so a new number would have duplicated it -- the T-383 mistake recorded in the HSJ hub. Its detail is folded into T-374 instead. Also appended to T-374 a refinement that matters to whoever implements it: Q5 of the decision document treats BOTH symptoms as reporting defects fixed by making one rooting authoritative at the boundary. Measured against tip a8fbba8, that holds for the reported-vs-TreeLength gap but NOT for the MPT-set symptom -- pool membership is selected at MaximizeParsimony.R:1618 on search-time scores taken at differing rootings, so canonicalising the report path leaves 2 distinct scores among 32 "equally parsimonious" trees. Recorded as the open residue of T-374, explicitly out of scope for the reporting fix.
…as invalid Withdrawn same day, before any fix was written, so nothing was built on it. The claim (pushed in 7d4d255) was that the MPT-set symptom survives a canonicalised report path, evidenced by "2 distinct scores (183, 182) among 32 MPTs at a common tip-1 rooting". The measurement 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 with the rooting taxon named explicitly (dev/red-team/heavy-tests/t385-diagnose-rooting.R): all 32 returned trees are already rooted at the kernel's tip 0 (32/32, root degree 2), and at a genuinely common rooting they all score 183 -- 32 distinct topologies, 0 disagreement. The pool IS self-consistent, so pool re-filtering is NOT required and n_topologies/collapse semantics do not need to move. That materially simplifies T-385. What survives is the P1 itself: reported 178 is not the score of the tree at the rooting it is returned at (183). And the objective is genuinely rooting-sensitive -- the same topology spans 178-183 over 8 rootings, inside the Sum nSec = 12 bound, with 178 attained at 2 of them. Fourth environment/measurement artefact this session (after %in%-on-Splits dispatch, ARM64 tip ordering, and covr timing). Same root cause each time: asserting on something incidental -- here "tip.label[1]" as a stand-in for a fixed taxon -- instead of naming the thing I meant. See [[loadall-is-not-rcmdcheck]] lesson 3.
Pre-fix reproduction against cpp-search tip a8fbba8, written before any fix so the failure is on record independently of the change that addresses it. The finding was recorded today but the tip has moved since (T-391 landed), and [[redteam-verify-against-current-tip]] says to re-verify rather than assume. All three symptoms reproduce on a 36-tip / 6-block / nSec=2 synthetic dataset (seed 1, search seed 11, maxReplicates 4): reported attr(res, "score") 178 TreeLength(res[[i]], ...) as returned 183 for all 32 trees -> gap 5 same topology, 8 rootings 178..183, spread 5 (bound Sum nSec = 12) 32 MPTs at a common tip-1 rooting 183 and 182 -> 2 distinct Note the reported 178 IS attained, by 2 of the 8 sampled rootings: the engine is not computing a wrong number, it is recording a score at one rooting and returning a topology rooted elsewhere (ts_collapse_pool's tip-0 canonicalisation being the trigger site named in T-374). The script reports rather than asserts, and exits 1 while the gap is open, so it doubles as the acceptance check for the fix.
Implements the ALREADY-DECIDED Option 3 of dev/plans/2026-07-29-t374b-xform-rooting-policy.md, whose three commits on cpp-search (7a18a4b et al.) turned out to be docs only -- no code implemented "make MaximizeParsimony's reported score and TreeLength() agree on one rooting". ## The defect XFORM's step matrix is asymmetric (gain = nSec + 1 against loss = 1), so a tree's length depends on where it is rooted -- unlike the symmetric criteria. MaximizeParsimony reported `result$best_score`, recorded mid-search at whatever rooting the replicate held, while ts_collapse_pool hands every tree back re-rooted on tip 0. So the reported number was not the length of the tree returned, and re-rooting a returned tree changed it again. Reproduced on tip a8fbba8 (36 tips / 6 blocks / nSec 2), pre-fix: reported 178 | TreeLength(returned) 183 on all 32 trees | gap 5 same topology over 8 rootings: 178..183, spread 5 (bound Sum nSec = 12) The reported number was not WRONG -- 178 is attained by 2 of those 8 rootings -- it was a value at a rooting the user never receives. ## The fix Canonicalise at both boundaries, on the dataset's first taxon (the tip-0 rooting ts_collapse_pool already imposes): - TreeLength(): root before scoring, in BOTH the single-tree and multiPhylo methods. The multiPhylo path previously rooted only trees that arrived unrooted, so an already-rooted tree kept its own rooting. Rooted by NAME with tips re-aligned afterwards, never by an R reroot on the edge matrix (na-validation-alignment-gotcha). - MaximizeParsimony(): rescore the returned pool through that same path and report the result -- |pool| evaluations. Post-fix the acceptance script is clean: gap 0, spread 0 across 8 rootings. Scores may therefore differ from previous versions and will not decrease; the value is now the length of the tree in hand, an upper bound on the rooting-free minimum exceeding it by at most Sum nSec (attained by 87-98% of rootings). Deliberately NOT done: min-over-rootings reporting. It is what the plan calls the better variant, but it reports a quantity the search never compared and costs (2n-3)x on the Sankoff term, so it stays Option 4. HSJ reporting is also untouched -- there rooting-invariance is REQUIRED by the method (two-state DP, T-374's open half), so canonicalising would convert a wrong objective into a stably-wrong one. Gated on XFORM alone, with that reason in the code. ## Scope boundary, and a warning where it bites Pool membership is still decided on search-time scores taken at differing rootings, so the returned trees need not share the canonical length. That is T-374's open residue, out of scope here -- but no longer silent: MaximizeParsimony now warns and reports the smallest. The warning immediately found a live instance in the existing suite (all-hierarchy data spans 7 to 9), which independently corroborates the earlier session's "4 of 6 trees do not share a score" observation. ## Tests Two regression tests, both verified to FAIL against the pre-fix tip built in a throwaway worktree (spread 2 not 0; reported 46 against min 48) and pass after: - The deterministic one carries its own anti-vacuity guard: it first ASSERTS via ts_sankoff_test -- untouched by this fix -- that the raw kernel really is rooting-sensitive on its 8-tip input, so it cannot pass by scoring rooting-insensitive data. - The end-to-end one asserts the actual contract (reported == min of the returned trees' canonical lengths), not the stronger "every tree shares it", which holds for its data but is not what the code promises. Also fixes the three false root-invariance comments the plan lists (ts_tbr.cpp:123 and :3152, ts_rcpp.cpp's collapse reroot, MaximizeParsimony.R's collapse note), and documents the behaviour in ?MaximizeParsimony, ?RecodeHierarchy, NEWS.md and vignettes/search-algorithm.Rmd. Suite green: xform, tree_length, hsj, t330, recode-hierarchy, resample-hierarchy, t306, prune-reinsert -- 0 failures.
… rename Not part of T-385 -- surfaced by running check_man() for it. 419168d changed the roxygen example from "one notch less / effort = -1" to "one notch more / effort = 1L" but man/MaximizeParsimony.Rd was never regenerated, so the shipped example contradicted its own source. Regenerated from the roxygen block, which is authoritative.
Coordination-only. The reporting half of T-374 is implemented on claude/t385-xform-report-agreement (stacked on PR #277): TreeLength() canonicalises the rooting in both its single-tree and multiPhylo methods, and MaximizeParsimony() rescores the returned pool through that path before reporting. Acceptance script: gap 5 -> 0, rooting spread 5 -> 0. Worth the row update on its own: the new "returned trees do not share a length" warning fired immediately on an EXISTING test -- all-hierarchy xform data returns a pool spanning 7 to 9. That independently corroborates T-374's original "4 of 6 trees do not share a score", which my own 36-tip attempt to reproduce had measured wrongly (retracted in 9296e87). The residue is real, reachable, and now visible at runtime instead of silent.
The acceptance script carried its own copy of MakeXformData() identical to the one the rooting diagnostic sources, so the two could silently drift. Sourced from the shared file instead. Acceptance script re-verified: exit 0, gap 0, spread 0 (checked WITHOUT a pipe -- the earlier 'EXIT: 0' was tail's status).
Three follow-ups, no behaviour change. - test-ts-xform.R's all-hierarchy test now expect_warning()s the "do not share a length" warning instead of emitting it into an otherwise-green suite. That matrix provably triggers T-374's residue (pool spans 7 to 9), so the warning is an expectation, not noise -- and if it ever stops firing, the residue has been fixed or masked and this test says so. Assign INSIDE expect_warning(): it returns the condition, not the expression's value, so `res <- expect_warning(...)` silently bound the warning object and broke the two assertions below it. - Both diagnostic scripts source() a path relative to the package root, so they only work when invoked from there. They now stop with that message instead of failing obscurely -- the same relative-path trap that made five tree_length tests "fail" earlier today when test_dir() changed the working directory out from under a relative lib.loc. - Recorded the real reason min-over-rootings reporting is declined. "Reports a quantity the search never compared" is the weaker half; the deciding objection is cost -- naively (2n-3)x on the Sankoff term, so a 4000-tip pool of 100 trees needs ~800k boundary evaluations. Affordably it needs an all-rootings up-down DP, which is Option 4's own piece of work.
All 8 CI jobs green on run 30662279563. The one ubuntu-24.04 (4.1) failure was transient -- it died in 'Set up R dependencies' with the Bioconductor 3.14 repos unreachable, so 'Check package' was skipped and no code was ever tested; green on re-run of that job alone.
T-385: report the x-transformation score of the tree actually returned
…f 30) Array 18126933, 150/150 cells at pin 644b5b1, every stopping rule left as shipped (maxSeconds = 0, maxReplicates = 96, targetHits = max(10, ntax/5)). Raw cells in dev/profiling/na-certify-stop-hamilton.csv. THE HEADLINE, and it corrects documentation written two commits ago: tripling `targetHits` changed floor attainment on NOT ONE of 30 matrices, while costing 2.58x the wall (26 of 30 slower, 22 by >10%, p = 6e-05). Only 33 of 150 cells were identical in score AND replicate count, so the runs genuinely went longer -- they just never found anything better. The mechanism is the disjoint- population argument measured at corpus scale rather than on one cell: on hard matrices the replicate cap binds before the hit target is reached (hitCapBound rises 22% -> 47%, i.e. the extra demand only pushes more runs into the cap), and on easy matrices the optimum is already in hand. The extra work lands exactly where it cannot help. The `effort` ladder doubles `targetHits` at rungs 5+, so this is evidence against a choice made in b2dfc3a6/2d871b41. It is kept -- without it a notch would be INERT on every dataset that stops early, and under implied weights it deepens the ratchet, which an equal-weights panel cannot see -- but the docs no longer imply it buys reach. Rungs 5+ now read as buying confidence and distinct trees on easy data, and reach on hard data. Gating still costs reach under the real stopping rules, so the default still does not flip: B_gate is 0 better / 4 worse (p = 0.125, uniformly negative), losses concentrated on the hard tail (Zanol2014 -0.6, Wortley2006 -0.4, Aria2015 -0.2, Zhu2013 -0.2). C_final is the arm worth pursuing: statistically indistinguishable from baseline (-0.013, 1 better / 2 worse, p = 1.000) at a THIRD of the wall, and its certifications are the productive ones -- 4349 of 11000 sweeps found a real improver (40%) against A_default's 9555 of 44889 (21%). Certifying the tree a replicate actually reports is four times cheaper and twice as likely to pay. It still regresses on Zanol2014 (0.6 -> 0.0), so it is a candidate, not a change to make now. Settling it wants the hard tail at more seeds, not another corpus-wide sweep: 24 of the 30 matrices sit at 1.0 in every arm and can only dilute the signal. Co-Authored-By: Claude Opus 5 <[email protected]>
…epos
R reads EITHER ./.Rprofile OR ~/.Rprofile, never both (?Startup). GHA steps
run with cwd = the checkout, so the root .Rprofile added on cpp-search (for
the Rcpp CRLF patch) silently suppressed the user profile -- which is exactly
where r-lib/actions/setup-r writes
options(repos = c(RSPM = <Posit PM>, CRAN = ...), Ncpus, HTTPUserAgent)
(see setup-r/src/installer.ts:659-757). Consequence on every cpp-search job:
Posit Package Manager never entered getOption("repos"), the User-Agent that
makes it serve binaries was never set, and pak fell back to its default CRAN
mirror -- resolving 100% of dependencies from source. ~10 min of compiling per
leg, and it also explains why the earlier Ncpus and RSPM/env experiments
measured nothing: those values are written into the same shadowed file.
Evidence (cache-immune -- pak prints this before installing anything):
main arm64 leg: repo_status() row 1 = RSPM <Posit PM>; 110 aarch64
binary resolution targets, 1 source
cpp-search leg: repo_status() row 1 = CRAN https://cran.rstudio.com,
Posit PM absent entirely; 0 binary targets, 101 source
Fix: chain to the user profile before installing the Rcpp hook, with a guard
for the cwd == home case. inst/Parsimony/.Rprofile is deliberately left alone
-- it ships in the tarball and has no bearing on dependency setup.
cache-version bumped to 2 on every setup-r-dependencies step: the v1 caches
were populated while the shadowing was in effect, so every package in them
was built from source. Retire them once.
Co-Authored-By: Claude Opus 5 <[email protected]>
Two cuts to the preliminaries on the fast legs, now that dependencies actually
resolve as binaries again.
1. Pin Posit PM to a dated snapshot (2026-07-30, the date proven to serve
aarch64/noble binaries) on `sense-check` and on agent-check's ubuntu leg.
`setup-r-dependencies` keys its cache on the RESOLVED dependency versions,
so `latest` misses the cache whenever any of ~110 dependencies gets a new
CRAN release -- most days -- and re-downloads the lot. A fixed snapshot
keeps the cache valid for weeks, so dependency setup becomes a restore.
`core`'s ubuntu legs deliberately keep `latest`: catching breakage from
bleeding-edge dependencies is the R-devel recipe's job, not the fast
"did this commit build?" signal's.
2. Drop the texlive apt step from `sense-check` (~45 s/run). It was never
needed: `check-r-package@v2` defaults to args = c("--no-manual",
"--as-cran") and build_args = "--no-manual", and all eight vignettes are
`rmarkdown::html_vignette`, i.e. pandoc-only. `core` and `full` still
install texlive, so a real LaTeX dependency would still be caught.
Co-Authored-By: Claude Opus 5 <[email protected]>
…s ~70 s/run Profiling the (now-binary) dependency step showed the download of 108 binaries takes 6 s and installing them ~10 s. The time is elsewhere: pak install + resolve + metadata DB ~17 s download 108 binaries 6 s pak system-requirements apt pass ~75 s <-- ppa:xtradeb/apps + Chromium install 108 binaries ~10 s MaxMin (GitHub source): Packaging 37 s + Building 29 s = 67 s shinytest2 -> chromote declares `chromium` as a system requirement, so on Linux pak runs `add-apt-repository -y ppa:xtradeb/apps`, `apt-get update`, then installs Chromium and its GTK/X11 tree. That happens on EVERY run: the sysreqs pass is not covered by the package cache (confirmed on a warm-cache run whose dependency step still took 83 s with 112/117 packages kept). No Linux leg needs it. shinytest2 appears only under inst/Parsimony/tests, which `.Rbuildignore` excludes, so it is not even in the tarball R CMD check examines; the app suite runs in the dedicated `shiny` job on Windows, which is left untouched. MaxMin's 67 s is deliberately NOT addressed here: it is a genuine trade (tests/testthat/test-WideSample.R would skip on the affected leg) and is the maintainer's call. Co-Authored-By: Claude Opus 5 <[email protected]>
…after) Answers whether a container with R (or R + all deps) preinstalled beats setup-r + setup-r-dependencies. Bar to beat: ~40-70 s for R alone, ~3 min for R + dependencies. Measures raw pull time/size for extrapolation, plus the runner's own "Initialize containers" step, which is the cost that actually lands on a container-based leg. Co-Authored-By: Claude Opus 5 <[email protected]>
…branch) Co-Authored-By: Claude Opus 5 <[email protected]>
…hypothesis RETRACTION. Panel 2's headline -- "targetHits escalation is pure cost, 0 of 30 matrices improved at 2.6x the wall" -- is true and useless. 24 of the 30 matrices are saturated at 1.0 in every arm, so "no improvement" there is the expected result, not a finding; and floor attainment is binary, so it cannot see an arm getting closer without arriving. I flagged the dilution in the same document and then led with the diluted number anyway. Re-analysed on score, restricted to the six matrices with headroom, and counting what the arm actually bought: extra replicates from targetHits x3, all 30 matrices : 4409 (4.2 CPU-hours) ... on the six matrices with headroom : 243 ... on Zanol2014 / Zhu2013 / Geisler2001 : 0 On the three hardest the 96-replicate cap bound BOTH arms on every seed (21 of 30 headroom cells), so A_hits3 did byte-identical work to A_default -- identical scores, identical replicate counts. 94% of the extra work landed on matrices that were already solved. Only 9 cells anywhere gained a replicate. So the correct claim is not "raising targetHits does not help" but "targetHits cannot act once maxReplicates binds", which is a stronger statement about mechanism and a much weaker one about effort. The hypothesis -- low effort misses the optimum, high effort takes longer and recovers a better score -- was never tested, because the arm could not act where it mattered. PANEL 3 tests it. Hard matrices only (the six with headroom), scored on SCORE not binary attainment, arms = `effort` 0/+1/+2 crossed with certification. Arms are `effort` itself, not a hand-rolled budget, because a first draft that raised only maxReplicates was INERT on Aria2015: targetHits stopped every run at 39 replicates, far below even the 96 cap, so all three budgets returned identical scores. That is the exact mirror of panel 2's mistake. Only moving both knobs escalates every dataset -- which is what the ladder does -- so testing the shipped argument is both more honest and more informative. Two things the smoke test exposed, recorded rather than silently patched: * Panel 2 pinned `strategy = "default"` for every matrix, so on 65-119-tip matrices it did NOT measure what a user gets (`auto` selects `thorough`). * Notch 3->4 is DEAD on hits-bound datasets: rungs 1-4 never touch targetHits and rung 4 raises only the cap, so `effort = +1` and `+2` gave literally the same run on Aria2015. Rung 4 is an auto pick calibrated for >=120 tips, so this is not restructured on no evidence -- panel 3 will quantify it. Co-Authored-By: Claude Opus 5 <[email protected]>
arm64, ubuntu-24.04-arm runner: rocker/r-ver:latest pull 10s, 865MB unpacked rocker/tidyverse:latest pull 25s, 3073MB unpacked container: rocker/r-ver -> "Initialize containers" = 12s (pull+create+start) => ~10s fixed + ~7ms/MB; image ships R 4.6.1 + 31 packages Conclusion: an R-ONLY image is roughly a wash -- setup-r spends only ~10s on R itself, its other ~52s being the hard-coded devscripts/ghostscript apt install. An image with R + all ~110 dependencies baked in would land near 1.3-1.6GB, predicting ~15-20s of container init against the 170-250s the two setup steps cost today. That is the variant worth building, if we build one. Incidental: rocker sets repos in Rprofile.site, to p3m.dev/cran/__linux__/noble/latest -- the rolling "latest", NOT a dated snapshot. Rprofile.site is read unconditionally, so unlike the ~/.Rprofile route it is immune to the project-.Rprofile shadowing this branch fixes. Whether it also sets HTTPUserAgent is unverified and would need checking before relying on it. Co-Authored-By: Claude Opus 5 <[email protected]>
MaxMin isn't on CRAN (Remotes: ms609/MaxMin), so pak packages+builds it from source on every cache miss. Costed hosting prebuilt binaries via ms609/packages (a new GHA workflow there, a repository_dispatch sender + cross-repo PAT in ms609/MaxMin, per-arch/per-R-version Linux tarballs since GitHub Pages can't do the UA-negotiation drat needs for Linux) -- MaxMin is pure Rcpp numerics with no OS-conditional code path, so that infrastructure would buy no coverage the Windows/macOS legs don't already give. ?ignore on the fast Linux legs (sense-check, core's two Linux configs, agent-check ubuntu) is the same 67s saving with a one-line change; test-WideSample.R already skip_if_not_installed and every WideSample.R call site is requireNamespace()-guarded, so those legs degrade gracefully. Windows and macOS legs keep MaxMin installed for real.
… check THROWAWAY verification step -- the previous cache-2 entry predates MaxMin=?ignore and still has MaxMin installed, masking whether the ignore directive actually works. Revert to 2 once confirmed.
…ignore Confirmed against a genuinely cold cache (run 30699889580): zero Packaging/Building MaxMin activity, R CMD check reports MaxMin correctly absent, test-WideSample.R skips via skip_if_not_installed, and dependency setup dropped from 174s to 95s -- consistent with removing the ~67s MaxMin build while ~99 other deps still resolve as binaries. Back to cache-version 2 now that the verification is done.
Both fixed together on feature/hsj-token-index-fix. T-374 (HSJ rooting) remains open and unaffected; noted that hsj-paper-oracle.R now reads 8 pass/0 fail (not 2/3) so a future T-374 session doesn't mistake that for a T-374 fix.
…ctors
Tested whether Vine's approach (bioRxiv 10.64898/2025.12.24.696405) -- embed
taxa, decode by neighbour-joining, backpropagate through the decoder -- ports
to TreeSearch. It does not, in either candidate slot.
LeastSquaresTree(): the refit discards the decoder's branch lengths, so
RSS-after-refit is piecewise CONSTANT in the input distance matrix (6 distinct
values over 241 grid points, median adjacent step 0). The only differentiable
surrogate misranks topologies (Spearman +0.10 to +0.36 among RF==2 neighbours),
and scale/affine correction moves that by <0.02, so it is not a branch-length
artifact. An oracle hill-climb on the TRUE objective tied NNI+SPR 3/5 and lost
2/5 -- it never won, so no gradient could help.
Sector starts (build_ras_sector): perturbed-NJ makes better individual starts
than RAS Wagner (2.5 vs 4.0 steps above optimum) but LOSES on best-of-5
(1 win / 7 ties / 7 losses) because its restarts are half as diverse
(CID 0.162 vs 0.328). Multi-start sector solving is diversity-limited, not
start-quality-limited. Corroborated internally: the more diverse pNJ arm beats
the less diverse one on every measure.
Two follow-up leads, unrelated to Vine, recorded in the write-up:
- LeastSquaresTree(method = "ols") returns topologies worse than the untouched
NJ start when rescored under NNLS, on 5/5 targets (20-48% worse). Expected
mathematically, but the documented purpose is Lapointe & Cucumel average
consensus, which is NNLS. Worth a sentence in ?LeastSquaresTree.
- src/ts_ls.{cpp,h} has no certify_unrooted equivalent -- it is the one search
path without the lever that dev/profiling/na-certify-gate.md documents.
Docs and benchmark scripts only; no package code touched.
Co-Authored-By: Claude Opus 5 <[email protected]>
…ignore Supersedes the MaxMin=?ignore approach: ms609/packages now publishes prebuilt MaxMin binaries for every platform/R-version this repo's CI matrix uses (github.com/ms609/packages/pull/1), so there's no longer a tradeoff to make -- every leg gets real MaxMin coverage without paying to compile it from source. - DESCRIPTION: drop `Remotes: ms609/MaxMin` (Remotes always wins over a repo binary in pak/remotes resolution, so it would otherwise force a from-source GitHub build regardless of what's in the repo) and add `Additional_repositories: https://ms609.github.io/packages` so pak knows to look there for a Suggests package absent from CRAN. - Windows/macOS legs need no workflow changes: they resolve MaxMin automatically via the standard drat bin/<os>/contrib/4.6/ layout now published there. - The 3 Linux legs (sense-check and agent-check's ubuntu-24.04-arm release, core's ubuntu-24.04 r4.1 and ubuntu-24.04-arm devel) each get a pinned `url::` ref to a stable, unversioned bin/linux/<tag>/MaxMin_latest.tar.gz -- GitHub Pages can't do the User-Agent negotiation Posit Package Manager uses to serve Linux binaries from src/contrib, so this is the only route there. Because the URL has no version/commit in it, it never needs updating here when MaxMin changes -- ms609/packages' build-maxmin workflow keeps the URL's content current on a schedule.
…naries
Root cause: a project-root .Rprofile (added for the Rcpp CRLF patch) shadowed
~/.Rprofile, where r-lib/actions/setup-r writes options(repos = c(RSPM =
<Posit PM>, ...)). Every job on this branch was therefore resolving 100% of
its dependencies from source against plain CRAN instead of Posit PM binaries
-- confirmed via pak::repo_status() and resolution-target counts (cache-
immune; wall-clock and Building counts are not, and cost a full session to
that confusion). Fixed by chaining to the user profile.
On top of that, three further cuts to the CI preliminaries, each verified by
mechanism rather than timing alone:
- Pin Posit PM to a dated snapshot instead of `latest` on the fast legs, so
the dependency cache doesn't miss every time a dependency gets a new
CRAN release.
- Drop the texlive install from sense-check: no leg needs it (--no-manual,
html_vignette everywhere).
- shinytest2=?ignore on Linux legs: its chromote/chromium sysreq forces an
apt PPA + Chromium install every run, uncached, for a suite that only
runs (correctly) on the Windows `shiny` job.
- MaxMin (Suggests, not on CRAN) now resolves from prebuilt binaries at
ms609.github.io/packages (new build-maxmin workflow there, PR ms609/
packages#1, merged) instead of building from GitHub source on every
cache miss. Remains a live, tested dependency on every leg -- not
ignored.
Dependency setup: ~10 min -> ~35 s warm cache / ~2 min cold, measured
end-to-end on ms609/TreeSearch CI, not locally.
Co-Authored-By: Claude Opus 5 <[email protected]>
Co-Authored-By: Claude Sonnet 5 <[email protected]>
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.
Manual testing underway; shiny app in particular has some usability issues.