Repository navigation
Fit cssLasso()'s second lasso path over an anchored, padded grid (#125) - #200
Conversation
cssLasso() is css()'s innermost hot path -- 2 * B calls per css() -- and it paid for two complete coordinate-descent path fits where one would do. The first fit produced the default path; predict.glmnet(exact = TRUE) then threw that fit's coefficients away, kept only its $lambda, and refit the whole path augmented with the target penalty, including every grid point BELOW it. Warm starts run in decreasing-lambda order, so those lower points are solved after the target and cannot influence it. The second fit now runs over the grid a new internal helper, anchoredLambdaGrid(), builds: the augmented grid's prefix through one element past the target, five geometrically spaced interior points, and the augmented grid's own final element re-appended. The re-append is the load-bearing part -- glmnet:::lambda.interp() normalises by lambda[1] - lambda[k], so the blend weight at the target depends on the last supplied grid point as well as the first, and truncating the grid changes the answer in its last bits even when both fitted columns are bit-identical. The selection is then read off with an ordinary exact = FALSE call. Output is unchanged bit for bit; the saving is wall-clock only. exact = TRUE, the do.call idiom that existed solely to make update() re-evaluable, and the now-unused newx / x / y arguments are removed, along with the comments that asserted they were load-bearing. The do.call fact survives in the one place that still exercises it, the identity block's reference fit. A short-circuit predicts straight off the default fit when the target penalty is already one of the fitted penalties. That is a correctness requirement, not an optimisation: removing it changes the selected set on the on-grid route, concentrated at the top of the path. css() never reaches it; it keeps the exported direct-call route byte-identical. Four new test blocks, with the division of labour spelled out in each: - "cssLasso is byte-identical to the exact refit (#125)" -- a pin, green on the pre-change source, over four fixtures (iid, correlated, n < p, bundled alpha), each asserting its own fixture rules: at least 1e-14 relative from every fitted penalty, and more than one feature selected. - "cssLasso fits the anchored grid rather than refitting exactly (#125)" -- the regression test. Both assertions are red on the pre-change source. - "anchoredLambdaGrid holds its invariants (#125)" -- what actually enforces the construction, including a mandatory ULP-spaced fixture without which the strictly-decreasing and no-duplicates pair cannot fail. - "cssLasso short-circuits when lambda is already on the path (#125)" -- with the rank-1 penalty in the fixture set and the mock assertion load-bearing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
) Both stage-6 passes read 96f02cf and converged on three comment defects; the drift sentinel found four more. None touches the code -- the gate, the suite and the bit-identity claim are unchanged. The three both passes found: - The identity pin's failure-diagnosis comment sent a reader to audit anchoredLambdaGrid() first. Measured, every mutation of the helper tried in review leaves that block green; only the on-grid branch predicate reddens it, and deleting the short-circuit cannot redden it at all, because all four of the block's fixtures assert themselves off-grid. The comment now ranks the two causes instead of conjoining them. - "both of the helper mutants" named a block that names three. Replaced with the predicate rather than a corrected count. - "both sides discard non-finite path entries": sort() drops NA and NaN and keeps +-Inf. The four the sentinel found alone: - Two published narrative sentences said the anchored grid solves only the part of the path that can influence the target. It deliberately does not -- it retains the padding points and the path's final penalty, which the interpolation reads and which the helper's own comment calls load-bearing. Shipped as written, the site would have argued for the one simplification the identity pin cannot detect. - The ULP fixture's detection threshold is n_interior = 5, not 2 as the comment implied; a reader lowering it would have silently restored the unfalsifiable assertions the fixture exists to fix. - The hi <= lo guard fires whenever s is the smallest OR second smallest element, a wider set than the comment enumerated. - @PARAM lambda_path's precondition was neither sufficient nor doing any work. The replacement was verified exact against the function's actual behaviour across 55 zero-bearing path and s combinations. Also: a half-line saying the second do.call is habit rather than necessity, since exact=TRUE at an on-grid s never reaches update(). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review — post-execution pass + drift sentinel, independent contexts, at
|
| battery | comparisons | mismatches |
|---|---|---|
cssLasso() randomized sweep (250 designs × up to 18 penalties; n 12–120, p 2–200; iid / collinear / block-correlated / orthonormal / constant-column / near-orthogonal; α ∈ {1, 0.2, 0.5, 0.9}; s from 0 to 1e6, including exactly min, between the two smallest, and above λ_max) |
4,500 | 0 |
| on-grid penalties (short-circuit route) | 1,939 | 0 |
coefficient vectors vs do.call + exact = TRUE |
360 | 0 |
css() end-to-end, feat_sel_mat + clus_sel_mat (48 runs, 70,884 cells) |
96 | 0 |
n_interior ∈ {0,1,2,5,20,100} invariance, coefficients and selected sets |
5,400 | 0 |
| second pass, off-grid configurations | 480 | 0 |
Maximum absolute coefficient difference exactly 0 throughout. n_interior really is a speed constant: varying it 0→100 changes nothing, so the roxygen claim holds.
Performance is understated, not overstated. Re-measured with 12 interleaved timings per cell: 18.8% (n=400/p=800) and 27.0% (n=200/p=100), Wilcoxon p ≤ 3.3e-05, against the body's 16.4% / 23.7%.
And the CI anomaly is not this change. Two Ubuntu jobs ran 16–21 minutes against ~3 for the rest, which I flagged at the outset. It's runner network variance in apt-get: Fetched 47.3 MB in 10min 10s (77.6 kB/s), where the same 47.3 MB took 20 s on the devel job in the same run. testthat.R times are normal and in line with the last main run.
1. Robustness — the speedup, which is the entire point of this PR, is unpinned
Replacing anchoredLambdaGrid()'s body with return(g_full) — no shortening at all, i.e. exactly the augmented grid exact = TRUE used to refit over — gives:
| tree | blocks | failed | error | WARN | passed |
|---|---|---|---|---|---|
6da0a53 |
178 | 0 | 0 | 0 | 4262 |
| no-shortening mutant | 178 | 0 | 0 | 0 | 4262 |
Byte-identical. I measured what that mutant costs on css() at B = 50, n=200/p=100: 0.848 s → 0.980 s, about 13.5% of the benefit gone, silently. A second pass measured 8–12% on its own fixtures.
This is not a correctness hole — every grid meeting the five invariants reproduces the refit's columns and weight, and the invariants block enforces those (see below). It is that a future refactor could delete the optimisation, or quietly neuter it, and nothing anywhere would fail. The routing block that looks like it pins shortening is self-referential by its own admission — index.Rmd:8690 calls the same helper production calls, so it compares the implementation to itself.
The cheap repair is one assertion: expect_lt(length(g), k_full) on the mid-path fixture in the invariants block at index.Rmd:8760, where that block's stated objection to expect_lt doesn't apply.
2. Robustness — the short-circuit block's comment is false about its own fixtures, and contradicts itself
index.Rmd:8806-8810 reads "THIS IS THE LOAD-BEARING ASSERTION OF THE BLOCK … Deleting the short-circuit leaves the identity assertions above green." Measured by mutation against a 0/0/4262 control: deleting the short-circuit reddens 3 of that block's 4 assertions, including the identity assertion at fA$lambda[1].
The block's own comment 16 lines earlier (:8790) says rank 1 is in on_grid precisely because breaks concentrate at the top of the path — the opposite conclusion. The concrete risk is a reader trimming the rank-1 fixture as redundant and removing the only value-level detector for that route.
3. Clarity — the anchor's necessity is floating-point, not mathematical, and the body's "1.4e-05" reads as though it were material
Two related things here, and the second explains the first.
The body says the unanchored variant shows "coefficient differences up to 1.4e-05", in the same sentence as "in its last bits". That absolute figure is scale-proportional: measured, the relative difference never exceeds ~2 ULP (at y-scale 1, 2.2e-16; at y-scale 1e3, 6.1e-13 absolute but 4.2e-16 relative). plan.md:136's "far too large to be last-bit noise" isn't supported. The shipped comment is right; only the body and the plan overstate. (The 99.6–99.9% agreement figure does reproduce — 479/480.)
The reason it is last-bit noise is worth putting in the source, because it also protects the anchor. lambda.interp does:
sfrac <- (lambda[1] - s) / (lambda[1] - lambda[k])
lambda <- (lambda[1] - lambda) / (lambda[1] - lambda[k])
...
sfrac <- (sfrac - lambda[right]) / (lambda[left] - lambda[right])The lambda[1] - lambda[k] factor cancels algebraically in that final ratio — the exact weight is just (λ_right − s)/(λ_right − λ_left), with no dependence on the grid's last element at all. In floating point it does not cancel:
grid ending 0.0625 → left=2 right=3 frac=0.20000000000000042744
grid ending 0.001 → left=2 right=3 frac=0.20000000000000000111
difference: 4.16e-16
Same neighbours, weight differing by one ULP — and neither equals the algebraic value (0.19999999999999995559). So the source comment's "the blend weight at s depends on the last element of the supplied grid as much as on the first" is true numerically and false algebraically. That matters: a maintainer who reasons it through symbolically will correctly conclude the anchor is unnecessary and delete it, reintroducing the drift. One clause — that the normaliser cancels in exact arithmetic and the anchor exists to make the rounding identical — makes the anchor's necessity legible instead of looking like an error.
4. Clarity — "on an exactly orthogonal design" names the wrong condition
index.Rmd:2278-2286 (generated anchoredLambdaGrid.R:63-66) attributes the degenerate NaN-then-zeros $lambda to an exactly orthogonal design. The actual driver is max(abs(crossprod(X, y))) == 0 — orthogonal and X ⊥ y. Measured: an orthonormal design with generic y returns an ordinary 63-element path, zero NaN, zero zeros. The repo's own #157 fixture (which satisfies both conditions) does reproduce NaN + 99 zeros → two-element g_full → the hi <= lo guard, exactly as described. Only the stated condition is too broad.
Cosmetic
- The "six-figure sweep" comment points at
.plans/, which is deleted at merge — cite PR Fit cssLasso()'s second lasso path over an anchored, padded grid (#125) #200 instead, so the evidence stays findable. - The body's two-decimal percentages overstate median-of-3 precision.
- Check 2 (NOTE mode per profile § 9):
fit_at_lambdafrom theelsebranch reachespredict.glmnetatcssLasso.R:91without the class check applied tolasso_modelat:61. Narrowed, not blinded.
What held up
- Every other comment claim tested true, and several are load-bearing:
lambda.interp's normaliser verbatim;predict.glmnet's ownmatch(s, lambda, FALSE)being the same predicate the short-circuit uses;g_fullmatching glmnet's construction operator for operator; warm-start independence from smaller penalties (2,400 fits, difference 0); short-circuit removal breaking 13/1867 on-grid penalties with 12 at path position 1 and max 3.9e-04 — genuinely material, so "far larger than last-bit noise" is right there;css()never reaching the short-circuit (0 hits in 720 instrumented invocations). ktalone is genuinely insufficient, which is the non-obvious half of the design: 199/360 user-supplied grids don't survive glmnet's round trip bit-for-bit, sosfalls between knots andrightis read. Variant scores against the exact refit: shipped 360/360,kt-only 350/360, no-lo342/360, naive truncation 337/360.- The invariants block is doing the real work. Mutants M1 (drop the re-appended
lo), M2 (ktinstead ofkt + 1), M3 (drop themidfilter) and M5 (reverse the interior sort) are each caught, all of them only by that block, with the identity pin green every time — exactly as the narrative says. The PR's claim that revertingcssLasso.Ralone reddens exactly two routing assertions is also confirmed. The division of labour between the four blocks is real and correctly described. - The
log(0)path is unreachable, and now has a number behind the disposition: across 400+ realglmnet()fits none produced a$lambdamixing an exact zero with ≥2 distinct positives; glmnet's grid stays strictly positive down to y-scale 1e-150 and errors below. The helper is@noRdand not exported. - Gotcha 7 does not fire, and the near-duplication is deliberate:
clusterLassoCore()predicts with nos, noexactand no second fit, so there is nothing to share. And the augmented-grid derivation appearing in both production and the test oracle should not be extracted into a helper — that independent copy is what makes thektmutant catchable. - Gate:
0 errors | 0 warnings | 0 notes,PASS 4262from 4194,spell_check0 rows,url_checkthe recorded four 403s, no new TODO,NAMESPACE/man//CITATIONunchanged, no$callread anywhere, and the committedcssr/is byte-identical to a freshRscript build.R.
Action items
- Pin the shortening —
expect_lt(length(g), k_full)in the invariants block (finding 1). Needs a rebuild. - Correct the short-circuit block's "leaves the identity assertions green" comment, which is false for its own fixtures and contradicts the comment above it (finding 2). Same rebuild.
- Say in the source that
lambda.interp's normaliser cancels in exact arithmetic, so the anchor exists to reproduce the rounding (finding 3) — and soften "up to 1.4e-05" in the body to a relative figure. Same rebuild for the comment; the body needs none. - Narrow "on an exactly orthogonal design" to orthogonal and X ⊥ y (finding 4). Same rebuild.
- Cosmetic: re-point the "six-figure sweep" citation at PR Fit cssLasso()'s second lasso path over an anchored, padded grid (#125) #200.
None of these blocks merge; (1) and (2) are the two I'd want in before it lands, since both concern what the suite can and cannot see.
Two independent subagent passes plus orchestrator-run mutation, timing and a lambda.interp cancellation analysis; full reports in .plans/issue-125-csslasso-anchored-grid/post_execution_review_v2.md and sentinel_feedback_v3.md.
Answers the maintainer's review round on PR #200. All five action items; each verified here before applying, and all four findings held up. (1) THE OPTIMISATION WAS PINNED BY NOTHING. Replacing anchoredLambdaGrid()'s body with return(g_full) -- the change deleted outright -- left the suite byte-identical at [ FAIL 0 | PASS 4262 ]. The invariants block could not see it because g_full satisfies all five invariants trivially: it IS the reference grid. Root cause is mine and traceable: plan review round 2 swapped expect_lt(seen$k, k_full) for a self-referential expect_identical to remove a coupling to the padding constant, and that swap deleted the only assertion pinning that the grid shrinks at all. inv() now takes `shortens`, set from measured behaviour at every fixture. Both directions are pinned, since returning the full grid is correct on the guard branch. Re-derived: the mutant now reddens 4 assertions where it reddened 0. (2) The short-circuit block claimed deleting the short-circuit "leaves the identity assertions above green". Measured, it reddens 3 of that block's assertions -- and the comment 16 lines above already said rank 1 was in the fixture set precisely because it does. The two contradicted each other; the first was written before the rank-1 fixture that round 1 asked for was added. (3) lambda.interp()'s lambda[1]-lambda[k] normaliser CANCELS algebraically -- the exact weight is (lam_right-s)/(lam_right-lam_left), independent of the last element. It does not cancel in floating point: over 1000 s values on two grids differing only in their last element, the weight differed in 74.3% of them by up to 6 ULP. The comment said the weight depends on the last element "as much as on the first", which is true numerically and false algebraically -- so a maintainer reasoning symbolically would correctly conclude the anchor is unnecessary and delete it. It now says the anchor reproduces the ROUNDING. (4) The degenerate-$lambda condition is max(abs(crossprod(X, y))) == 0 exactly, not "an exactly orthogonal design": measured, orthonormal X with generic y gives an ordinary path, and so does one with max|X'y| at 1e-16. (5) The "six-figure sweep" comment cited .plans/, which is gitignored and deleted at merge. Now cites PR #200. Gate: 0 errors | 0 warnings | 0 notes; spell_check 0 rows; url_check the recorded four 403s; suite [ FAIL 0 | WARN 0 | SKIP 0 | PASS 4272 ] from 4262. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A narrow, suspicion-scoped round on the one question this cycle keeps getting wrong: does any comment claim a block catches something it does not? It does not re-review the design. Sixteen mutants, each grep-verified as applied before measuring, against a [ FAIL 0 | PASS 4272 ] control. Two of the three are residue of the commit before this one, which is the finding that matters. 1589024 corrected "both of the natural simplifications" at index.Rmd:2342 and left the identical sentence at :8569 -- the fifth time this cycle a fix landed on the sentence a finding quoted rather than on the claim it was about, and this time the finding being answered was itself about that pattern. - :8569 now names the six mutations measured to leave the identity pin green, rather than carrying a count. The number has been wrong at two and at three and would have been wrong again at six, so the fix is to stop counting. - :2304 claimed the #157 fixture reaches the degenerate-path guard branch. Instrumented, that block makes ZERO calls to anchoredLambdaGrid(): protolasso and clusterRepLasso route through clusterLassoCore(), which never calls cssLasso(). Across the whole suite, 5376 helper calls and none with a degenerate path. The mechanism claim is true and was verified by direct call; the coverage claim was false. The comment now records the gap rather than papering over it -- no test here exercises that branch. - :8619 blamed the identity pin's weak detection on its fixture COUNT. Measured, naive truncation breaks bit-identity on ~2% of off-grid penalties while reproducing the reference's selected set in 600 of 600 comparisons: the pin compares type="nonzero", so the limit is granularity and more fixtures buy nothing. The comment was telling a reader to do the one thing that cannot help. Comment-only; the suite is unchanged. The round's other question -- can every assertion in the four new blocks fail? -- came back clean: all fourteen sites were made to fire. Gate: 0 errors | 0 warnings | 0 notes; spell_check 0 rows; url_check the recorded four 403s; suite [ FAIL 0 | WARN 0 | SKIP 0 | PASS 4272 ]. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
All five action items are in, across two commits ( 1 — the speedup was pinned by nothingConfirmed: The root cause is mine and traceable. Plan review round 2 objected that I tested your proposed assertion against all ten fixtures before adopting it: it holds on the four that shorten and fails on four that legitimately don't (guard branch — 2 — the short-circuit commentConfirmed: deletion reddens 3 assertions, not 0. Same provenance — round 1 asked for the rank-1 fixture so that the identity assertions would fire; I added it, wrote the 3 — the algebraic cancellationThis is the sharpest finding and I had the mechanism wrong. Verified both halves: the normaliser cancels to On 4 — the orthogonality conditionConfirmed, and narrower than stated: an orthonormal X with A narrow round after these, and it was not cleanSince coverage claims had been corrected four times, I ran a round scoped to two falsifiable questions rather than another full pass. It found three more — and two are residue of The substantive one: the comment claimed the Third: I blamed the pin's weak detection on fixture count. Naive truncation breaks bit-identity on ~2% of penalties but reproduced the reference's selected set 600/600 — the limit is granularity, so my comment was telling a reader to do the one thing that can't help. Its other question came back clean: all fourteen assertion sites in the four new blocks were made to fire. On your re-measurementYour 18.8% / 27.0% is methodologically better than mine. You interleaved 12 timings per cell; I ran all "before" then all "after", which is exposed to drift between the halves. I've kept 16.4% / 23.7% in the body as the conservative pair I can attest to from this tree — happy to quote yours or a range instead. One caution about my own verificationMy first mutation battery reported both mutants green — a clean false null. A Gate at |
Adds the package's first NEWS.md, with a bullet for this branch's change. WHERE IT LIVES. cssr/ is generated and build.R deletes it wholesale, so a hand-written cssr/NEWS.md would not survive a build. The file is edited at source-files/NEWS.md and copied into the package by the inst/ chunk in index.Rmd, alongside CITATION and WORDLIST. The copy sits ahead of litr::add_pkgdown() in document order because that chunk builds the pkgdown Changelog page, and `news` is added to the navbar in source-files/_pkgdown.yml so the page is linked rather than orphaned -- the failure mode #53 fixed for reference pages. THE HEADER CARRIES THE VERSION, AND HAS TO. The skill's prescribed `# <pkg> (development version)` does not parse: measured with tools:::.build_news_db_from_package_NEWS_md(), it yields NULL, so R CMD check reports "Problems with news in 'NEWS.md': No news entries found" as a NOTE -- and under this repo's error_on = "note" gate, and the matching CI error-on: '"note"', a NOTE fails all five jobs. Measured alternatives: # cssr (development version) NULL entries -> NOTE # cssr 0.4.7.9000 1 entry -> OK # cssr 0.4.7.9000 (development version) 1 entry -> OK The third form is used: it parses, prints cleanly through news(), and keeps the development-version signal. Prose outside a version section is harmless; the header alone decides it. The cost is that the version now lives in two places, index.Rmd and this header, so a release bump edits both. .workflow/PROFILE.md section 4 previously recorded "Other files that must match: none" and now records the duplication, the measurement above, and that source-files/NEWS.md is the file to edit. The version itself is untouched at 0.4.7.9000, already a development version. spell_check reads root news markdown and passes with no new WORDLIST entries. Gate: 0 errors | 0 warnings | 0 notes; spell_check 0 rows; url_check the recorded four 403s; suite [ FAIL 0 | WARN 0 | SKIP 0 | PASS 4272 ]. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Added a Where it lives. The header has to carry the version, and that has a cost. The
The consequence: the version now lives in two places, The version itself is untouched at Gate at |
…#125) Adds test_that("cssLasso survives a degenerate lasso path (#125)"), at the maintainer's request. WHAT IT HOLDS. The Decision Log records that cssLasso() adds no runtime validation of lasso_model$lambda, justified by a measurement: a positivity check would abort on a design whose glmnet path is degenerate, where the function currently returns integer(0) -- the shape #157 was filed to stop crashing. Nothing in the suite held that measurement. Now something does. Measured, against a [ FAIL 0 | PASS 4276 ] control before and after: stopifnot(all(lasso_model$lambda > 0)) -> ERR 1, this block only stopifnot(all(is.finite(lasso_model$lambda))) -> ERR 1, this block only Both clauses of the contract the pre-implementation sentinel proposed are false on this path, so either spelling of the guard is caught. WHAT IT DOES NOT HOLD, contrary to my first reasoning. I expected the helper's `hi <= lo` guard clause to be unpinned. It is not: removing `| hi <= lo` already reddens the invariants block with 3 failures and 1 error, because the shortens=FALSE fixtures added in the review round take that branch on ordinary paths and the interior points then collapse onto the anchor. That hole was closed as a side effect of the review round. This fixture is narrower than it first looked -- it pins a decision, not the guard. The two path-shape assertions are an upstream-drift detector in the same spirit as the identity pin: if a future glmnet stops returning NaN-then-zeros for an exactly-orthogonal design, they fail and the helper's comment about that case needs re-deriving rather than quietly becoming decorative. The fixture uses a strictly positive penalty. At lambda = 0 this design raises a PRE-EXISTING interpolation error, on this branch and on origin/main alike, which is not what the block is about. Also corrected in the same edit: the helper's comment said "no test in this suite exercises this branch with a degenerate path", true when written two commits ago and false the moment this block lands. Gate: 0 errors | 0 warnings | 0 notes; spell_check 0 rows; url_check the recorded four 403s; suite [ FAIL 0 | WARN 0 | SKIP 0 | PASS 4276 ] from 4272. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Added the degenerate-path fixture ( It holds a decision, not the guard — and my first reasoning about it was wrong. I pitched this as closing a hole in the What was unheld is the Both clauses of the contract the sentinel proposed are false on this path, so either spelling of the guard is caught. The two path-shape assertions are an upstream-drift detector in the same spirit as the identity pin: if a future glmnet stops returning Two details worth knowing. The fixture uses a strictly positive penalty — at Gate at |
cssLasso()runs2 * Btimes percss()call and fit the full lasso path twice on everysubsample:
predict.glmnet(..., exact = TRUE)discards the first fit's coefficients, keeps onlyits
$lambda, and refits the whole augmented path. This keeps the first fit — the only way tolearn glmnet's truncated grid — and shortens the second.
What changed
A new internal
anchoredLambdaGrid()builds the second fit's grid: the path down to the targetpenalty plus one, five geometrically spaced padding points, and the original path's final
penalty re-appended. That last element is load-bearing.
glmnet:::lambda.interpnormalises bylambda[1] - lambda[k]. That factor cancels algebraically — the exact weight is(λ_right − s)/(λ_right − λ_left)— but it does not cancel in floating point, so the lastelement moves the weight by a couple of ULP. Tiny, and material anyway:
nonzeroCoef()testsabs(x) > 0with no tolerance, so a wobble around zero flips membership, and the unanchoredversion agrees on only 99.6–99.9% of selected sets. Warm-started coordinate descent cannot
depend on smaller penalties, so the points between are pure waste.
exact = TRUE, thedo.callidiom and three arguments that existed onlyto serve it are removed; the branch for a target penalty already on the fitted grid is a
correctness requirement, not an optimisation.
No signature, export or
man/change. This PR also starts aNEWS.md— edited atsource-files/NEWS.md, copied in by the build — with a bullet for this change.Evidence
Output is bit-identical, and that is the whole promise. Across review: 170,784 comparisons in
the feasibility sweep, 2,178 corner cases, and 2,592 fresh calls against
main— 0mismatches, maximum absolute coefficient difference exactly 0.
Measured at the final commit, median of 3,
B = 50:identical(feat_sel_mat)andidentical(clus_sel_mat)TRUE in both.On the gap from this issue's headline: #125 advertises 35–45%. That figure is the ceiling for
dropping the second fit entirely, which is not output-preserving — the feasibility
spike ruled
that out.
16–24% is what a byte-identical route buys.
Gate:
0 errors | 0 warnings | 0 notes,spell_check0 rows,url_checkthe recorded403s. Suite
[ FAIL 0 | WARN 0 | SKIP 0 | PASS 4272 ], from 4194.Resolves #125.
🤖 Generated with Claude Code