Skip to content

Predict at the penalty glmnet fitted, so cssLasso() stops selecting interpolation debris (Closes #199) - #201

Merged
gregfaletto merged 11 commits into
mainfrom
issue-199-csslasso-snap-penalty
Sep 9, 2026
Merged

gregfaletto merged 11 commits into
mainfrom
issue-199-csslasso-snap-penalty

Conversation

@gregfaletto

@gregfaletto gregfaletto commented Sep 8, 2026 •

Copy link
Copy Markdown
Owner

glmnet does not echo a supplied penalty back verbatim — it returns it after dividing by the
response scale and multiplying back — so the s cssLasso() predicted at could miss the fitted
entry by an ULP. glmnet:::lambda.interp() then blended two adjacent path columns, and
nonzeroCoef() selected the leak because its test is abs(x) > 0. Issue #199's reproduction
returns 1 2 6 with feature 1 at 2.468e-17 against a next-smallest of 0.0376; on this branch
it returns 2 6.

A new internal snapLambdaToGrid() returns the fitted entry when one sits within a few ULP of
the request, so lambda.interp() gives left == right with frac == 1 and the vector read back
is a solved lasso column with exact zeros. No coefficient-magnitude threshold is introduced: the
tolerance sits on the penalty scale, bounded by IEEE arithmetic. The sibling routes are untouched.

What changes for users

Selected features move where glmnet's reported penalty differs from the requested one in its
last bits — uncommon, and likelier the nearer the penalty sits to the top of a subsample's own
path. At cssLasso() the change removes features and does not add them: the blend's support
is a superset of the solved column's, and a feature the solved column carries survives a weight
indistinguishable from 1 or from 0. The only escape is exact cancellation, unobserved in 239,004
runs — so this is well tested, not proved, and NEWS.md says which.

At css() it can go either way, because the aggregation above cssLasso() is not
order-preserving: a lowered selection proportion can reach a tie resolved by returning more.
Two routes, and only one announces itself. getSelectedClusters() widening selected_clusts
warns, and under options(warn = 2) a call that returned can now error. A within-cluster
weighting tie widens selected_feats — what cssPredict() consumes — silently, with the
cluster set unchanged. Both are measured and both are in NEWS.md. This supersedes #125's
identity, and no math the paper specifies is touched.

The #156 block is one such case; its warning is muffled at both wrapper calls by the pattern
PR #186 introduced, leaving every other warning visible and its assertions untouched.

Verification

check 0/0/0, spell_check 0 rows, url_check the four recorded 403s,
[ FAIL 0 | WARN 0 | SKIP 0 | PASS 4297 ] against a base of 4276, CI green on all five jobs.

Under the mutation helper present / call site absent, exactly two assertions redden — the
routing block's wiring assertion and the end-to-end block's structural assertion — and nothing
else moves. WARN 0 is met by the muffle, not for free: base WARN 0 → wired WARN 1 →
muffled WARN 0.

🤖 Generated with Claude Code

gregfaletto and others added 7 commits September 7, 2026 23:34
cssLasso() handed predict.glmnet() the penalty it requested. glmnet does not
echo a supplied penalty back verbatim -- it reports the grid divided by the
response scale and multiplied back, two correctly-rounded scalar operations --
so the requested value can sit an ULP away from the entry the fit holds.
lambda.interp() then reports frac a hair short of 1 and predict.glmnet() returns
a blend of two adjacent path columns, leaking the neighbour's coefficients in at
around 1e-17. nonzeroCoef()'s membership test is abs(x) > 0 with no tolerance,
so those leaks are SELECTED and enter feat_sel_mat as ordinary selections.

snapLambdaToGrid() recovers the fitted entry within a few ULP of the requested
penalty; cssLasso() predicts there, on both the short-circuit and the refit
routes, so the coefficient vector it reads is a solved column with exact zeros.

Three new blocks: the routing block (the red-green test, mocking both glmnet
bindings so the nudge is deterministic), the helper invariants block (the
arithmetic, including the two fixtures that make the relative tolerance
load-bearing), and the end-to-end block (the solved column's nonzero set).
The #125 identity pin keeps its assertions and narrows its stated property.

The snap changes one selection in the #156 block's fixture, which drops a
cluster's selection proportion into a max_num_clusts tie; both of that block's
wrapper calls get PR #186's targeted muffle so the suite stays at WARN 0.

Suite 4276 -> 4297, FAIL 0 | WARN 0 | SKIP 0. Gate: 0 errors | 0 warnings |
0 notes (with _R_CHECK_SYSTEM_CLOCK_=FALSE; the recorded timestamp NOTE fired).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"The four blocks below" was true when four blocks followed it. The #199 blocks
land after them, so the sentence now names what it introduces without counting
what comes after, and points at the two narratives further down.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both were re-derived against the final tree rather than carried from the plan.

The helper's body said adjacent glmnet penalties sit "a relative 0.08 or so"
apart; measured on default paths at n=80/p=40 and n=40/p=120 the median
relative gap is 0.0975 and 0.0476, so the comment now quotes the range. The
order-of-magnitude conclusion (1e14 ULP) is unchanged.

The end-to-end block named three #125 blocks as independent cover for the
routing it is blind to. Measured, the never-refit mutant reddens a fourth:
this cycle's own routing block. Naming it matters, because the bullet exists
to stop a later reader trimming one of them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The helper's comment said "a positivity OR FINITENESS stopifnot() would turn
the degenerate path ... from an empty selection into an error", carrying over
a claim that is true of cssLasso()'s FIRST fit and not of the vector this
helper receives.

Measured on the design test_that("cssLasso survives a degenerate lasso path
(#125)") drives: the first fit's $lambda is NaN followed by zeros,
anchoredLambdaGrid() reduces it to c(0.01, 0), and the second fit returns
exactly that. So what reaches snapLambdaToGrid() is finite and NaN-free but
carries a zero. A positivity check therefore reddens that block (confirmed by
mutation -- it errors there and in the s = 0 fixtures of the invariants block);
a finiteness check does not.

The invariants block's NaN comment carried the same slip and is corrected the
same way: none of NaN-mixed, all-NaN or empty is reachable through cssLasso()
today, which is why the fixtures state a contract rather than a live path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The routing block said an on-grid penalty "reads 10 there". Re-derived: 10 is
just the index of whichever on-grid penalty is substituted -- m0$lambda[12]
reads 12 -- so the number was a property of one arbitrary substitution rather
than of the fixture. Restated as what is determinate: it reads L's own index in
the first fit's path rather than 0. The inert-mock reading of 27 does reproduce
on this fixture and is kept.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ts (#199)

The post-execution review and the sentinel's post-implementation pass each found
comment prose asserting things the tree contradicts. Both passes' findings are
applied here as one revision.

The no-validation comment said a finiteness check would not redden the suite.
Measured, it does -- the #199 invariants block drives a NaN-bearing grid straight
into the helper. Its enumeration of which check reddens which block has now been
wrong twice, so this states the rule in the positive instead: any validation
added here reddens the suite, with no enumeration left to get wrong.

The end-to-end block said that if the nudge stopped applying, the requested
penalty would be in the grid and the block would stop distinguishing snapped from
unsnapped. Measured with both nudges disarmed, the guard still reads 0 and the
block still separates 1 3 4 from 1 4, because this fixture's natural round trip
is already inexact here. Rewritten to say what the guard is for -- the platform
where neither source of inexactness is present -- and to say plainly that it
cannot see the nudge alone go away.

Both mocking blocks now name each other as the other holder of the same mock and
the same firing condition, since that condition is a claim about how cssLasso()
spells its second glmnet() call and a refactor of it disarms both at once.

Two figures are deleted rather than corrected: the muffle's jittered-draw counts,
which carried no scheme and did not reproduce under a plausible one, and the
adjacent-penalty spacing range, whose low end was outside it. What survives in
each case is the claim that is load-bearing and cannot rot.

NEWS.md is reconciled. Its #125 bullet claimed the anchored grid returns "exactly
the same selected features", which this change falsifies; it now says the same
features as the refit it replaced, which was true then and stays true. The #199
bullet is one sentence plus a scope clause, and says it supersedes that identity
rather than sitting beside it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A narrow review round checked 55 claims in the comment prose this branch adds
and found ten that do not hold. Every one is the same move: a measurement taken
over a coarse or convenient sample, then written down as a law. The repair in
each case is a narrower claim, not a better number.

The largest reached users. "Near the top of the path, which is the only place a
blend can materialise at all" came from a sample whose minimum was 0.643 of
lambda_max. Measured over 30 designs, blends occur down to 0.0533, and of the
selections that change most sit below half of lambda_max. The mechanism is a
gradient rather than a threshold: lambda.interp()'s numerator lambda[1] - s
absorbs more of an ULP-scale perturbation the further s sits below lambda[1],
which makes a blend progressively less likely, never impossible. Corrected in
the fixture comment and in NEWS.md, whose bullet renders on the public changelog
and now says the change is rare, arises anywhere on the path, and was in every
observed case a feature no longer being selected.

"ANY validation added here reddens the suite" was written last round to replace
an enumeration that had been wrong twice, and is itself wrong:
stopifnot(is.numeric(lambda_fitted)) leaves the suite byte-identically green.
The comment now names the two shapes that do redden, names the one that does
not, and says outright that this is not a rule about validation in general.

Seven smaller ones: the red-green block is not the only one that reddens under
its own mutation; an inexact round trip is not on its own enough to redden the
byte-identity pin (measured, 97% of such fixtures stay green); the roxygen
described an insensitivity the body calls one-sided; lambda.interp()'s
normaliser was named as the numerator where the rest of the file uses that
phrase for the denominator; the n_ulp lower bound is the measured 2 rather than
the non-operative 0.5; the muffle comment contradicted itself three sentences
apart, and is rewritten whole because a previous edit had left it saying the
same thing three times; and the absolute-tolerance mutant first diverges at
magnitude 6, not 8 -- the earlier figure came from testing 4, 8 and 1e6 and
never 5, 6, 7.

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

Copy link
Copy Markdown
Owner Author

Review — post-execution pass + drift sentinel, independent contexts, at 5ac1668

Verdict: one thing to resolve before merge — the direction claim in NEWS.md and the PR body — and it needs your adjudication rather than mine, because the two passes and I did not converge on how bad it is. Everything else is in good shape, including the question I was most worried about.


The thing I was most worried about is clean: #125's guards were not hollowed

This PR explicitly supersedes PR #125's bit-identity guarantee, and #125 shipped a suite built to pin exactly that. A later PR quietly relaxing an earlier PR's guard is this repo's most-repeated failure, so both passes went at it independently.

Not one assertion in any #125 block changed. The identity block gained 15 lines of comment and nothing else; the invariants, anchored-grid, short-circuit and degenerate-path blocks are byte-identical to origin/main. Seven mutants were scored on both trees:

mutant base head
drop the re-appended lo 5 fails 5 fails
prefix through kt not kt+1 6 fails + 1 error same
drop the mid filter 3 3
reverse the interior sort 4 4
no shortening at all 4 4
never short-circuit 3 3
never refit 3 blocks 4 blocks

Identical everywhere except the last, where the guard set is one block stronger. All four identity fixtures round-trip exactly, which is why their assertions did not need to move — the snap is a no-op on them. The pin was re-aimed with prose, not relaxed.

The block's name, "cssLasso is byte-identical to the exact refit (#125)", does now overstate what it pins — but its first comment line is WHAT THIS PINS, since #199 narrowed it, so a reader lands on the correction immediately. I'd leave it; renaming breaks ten cross-references that quote it verbatim.

The crossed-call hazard is covered

snapLambdaToGrid(lambda_fitted, s, n_ulp = 4L) sits directly below anchoredLambdaGrid(lambda_path, s, n_interior = 5L) — same arity, same second formal, both taking a numeric vector then a scalar, both defaulting to a small integer. The source comment says the differing first-formal name is "the one signal at a call site," which would be thin protection. It turns out not to be the protection: all five crossings redden, none silently — including the confusion the comment is actually about (handing the helper the first fit's path), which reddens the same two assertions as deliberate un-wiring.

One caveat worth acting on: for the crossing that puts snapLambdaToGrid at the anchored site, the entire margin is a single incidental #125 assertion, and the byte-identity pin stays green. stopifnot(length(s) == 1L) closes it, is about s rather than lambda_fitted so it doesn't conflict with the documented no-validation decision, and was measured to cost nothing (PASS 4297, byte-identical).


1. The direction claim is guaranteed at cssLasso() and only observed at css() — and the sentence covers both

source-files/NEWS.md:11-14 and the PR body say the change is rare and that "every change observed was a feature no longer being selected", with the subject being "which features cssLasso() and css() return."

At cssLasso() that is stronger than observed — it is structural, and worth saying so, because it turns a "we looked and didn't see any" into a guarantee. predict.glmnet() returns beta[,left]*frac + beta[,right]*(1-frac), and within the snapping tolerance frac is within a few ULP of 1, so the snapped column is the dominant one. For any feature: present in the solved column only → survives as frac*x; present in the neighbour only → leaks in at (1-frac)*x ≈ 1e-17 and is selected, because nonzeroCoef()'s test is abs(x) > 0. So the blend's support is a superset of the solved column's, and snapping can only remove. The one escape is exact cancellation between two opposite-signed contributions — a measure-zero tie. I checked it over 2,400 penalty reads: 0 additions, 0 cases where the snapped set was not a subset. A separate 3,000-run sweep found 297 inexact round trips, 17 changed, 17/17 removals.

At css() it is not structural, and this is the real point. The aggregation step is not order-preserving: per-subsample selections feed cluster selection proportions, and getSelectedClusters() under an auto max_num_clusts resolves a tie by returning more clusters. So a removal upstream can lower a proportion into a tie and widen the returned set. One pass measured exactly that — 400 default-path runs, 14 changed, 6 pure additions, with a worked example (base 11 → head 2 11, c10 falling 0.110 → 0.080 while myclust held at 0.080) and two undisclosed consequences: the Returning more than max_num_clusts warning firing on ordinary input, and cssSelect() erroring under options(warn = 2) where it previously returned.

I could not reproduce that, and you should know it before deciding. Across two configurations — 150 runs at n=40/p=12 with a strong signal, and 300 runs at n=22/p=14 with a deliberately tie-prone weak signal — I saw 3 changes, all removals, zero additions. A 300-run warning probe fired the max_num_clusts warning less often on head (5) than on base (6), not more. So the mechanism is real a priori and one pass measured it, but I have not seen it, and the rate and the two consequences rest on that one pass's configuration.

Either way the sentence needs narrowing, independent of how the disagreement resolves: the guarantee exists one level down and is asserted one level up, where a documented tie rule can invert it. Say the direction holds for cssLasso() and that css()-level changes were only observed as removals — or attach the reproducing seed and configuration and state the css() behaviour precisely. This sentence was added by 5ac1668, after the last review round, generalising a cssLasso()-only sweep — which is exactly why it is the one thing here that hadn't been looked at.

2. Robustness — correction residue, the sixth instance of this cycle's pattern

5ac1668 corrected "is the only one that can be" in the narrative at index.Rmd:9056 and left the identical claim nine lines below at :9065 ("THE RED-GREEN TEST FOR #199, and the only block that can be one"), where it is contradicted by its own next-but-one sentence and by measurement. This is the same fix-the-quoted-sentence-not-the-claim pattern your own 3c9487b on #200 flagged as having happened five times; the sentinel's sweep of the other five corrections found no residue, so this is the only one.

3. Notes

  • The max_num_clusts muffle is now in its third identical copy. Unlike the stopifnot family, withCallingHandlers deparses nothing, so this one is safely extractable into a test helper.
  • cssLasso() asserts the class of its first fit and nothing about its second; since cssLasso() can return a feature whose coefficient is a ~1e-17 interpolation artifact #199 the second fit's $lambda is consumed by snapLambdaToGrid() as well as predict.glmnet(), so it now silently assumes a numeric vector with at least one non-NaN entry. The comment names this rather than asserting it, deliberately — reasonable, and correctly disclosed.
  • The #156 block still tests its name after its selection moved, but its cssSelect() half is vacuous today (a disclosed trade).
  • "One in five hundred off-grid penalties" measured 1 in 176 in one sweep — the body is conservative in the wrong direction for a rate.

What held up

  • Twenty falsifiable claims tested, twenty held, several bit-exactly: the issue reproduction (base 1 2 6 with feature 1 at 2.4678e-17 against 0.0375864; branch 2 6); the round-trip mechanism reproduced under identical() as fl(fl(s/ys)*ys); nonzeroCoef()'s abs(beta) > 0; lambda.interp() going from left 4 / right 5 / frac 0.99999999999999678 to left 4 / right 4 / frac 1; default-grid spacing of 2.05e14–4.39e14 ulp against the claimed "1e14".
  • The tolerance is genuinely pinned from below and unbounded above, as claimed: n_ulp = 1 reddens 3 blocks, 2 reddens 1, and 3/5/100/1e6 are all green. s = 0 is reachable and safe — glmnet echoes 0 exactly, and the relative tolerance collapses to exact equality there, which is the right behaviour.
  • The no-validation decision is exactly as documented: all(lambda_fitted > 0) errors 2 blocks, all(is.finite(...)) errors 1, stopifnot(is.numeric(...)) is byte-identically green. length(j) == 1L is genuine control flow — TRUE && in its place errors on the all-NaN case.
  • The sibling routes really are untouched. The only other production predict.glmnet call passes no s at all, and no vignette calls it.
  • The muffle is the house pattern already on main, is load-bearing (WARN 1 without it), and an injected unrelated warning still surfaces through it. The 0 → 1 → 0 WARN chain reproduces on all three legs.
  • Gate: 0/0/0 exit 0, spell_check 0 rows, url_check the four recorded 403s, suite 4276 → 4297, CI green 5/5, and a fresh Rscript build.R reproduces the committed cssr/ byte-identically, LitrId included. NEWS.md parses to one entry and the generated copy was regenerated rather than hand-edited.

Action items

  1. Narrow the direction claim in NEWS.md and the PR body to what is actually guaranteed — structural for cssLasso(), observed-only for css() (finding 1). If the css()-level additions reproduce for you, disclose the warning and the warn = 2 behaviour too; if they don't, say what was swept. No rebuild for NEWS; the body needs none.
  2. Fix the residue at index.Rmd:9065 (finding 2). Needs a rebuild.
  3. Add stopifnot(length(s) == 1L) to snapLambdaToGrid() — measured free, and it converts the crossed-call margin from one incidental assertion into a direct failure. Same rebuild.
  4. Optional: extract the thrice-copied muffle; consider stating the cssLasso() direction guarantee in the source, since it is provable and currently reads as empirical.

Only (1) should gate the merge, and it is a sentence.

Two independent subagent passes plus orchestrator-run verification: a 2,400-read subset probe, a 450-run cssSelect() sweep across two configurations, and a 300-run warning probe. Full reports in .plans/issue-199-csslasso-snap-penalty/post_execution_review_v3.md and sentinel_feedback_v3.md.

gregfaletto and others added 2 commits September 8, 2026 09:44
…ler fixes (#199)

All four review points, in one commit because every commit on this branch carries
its regenerated cssr/ and docs/ tree and splitting would mean three more full
rebuilds. Each point is a paragraph below.

(1) THE DIRECTION CLAIM. "Every change observed was a feature no longer being
selected" was measured on cssLasso() and written with css() as its subject. Both
halves needed correcting.

At cssLasso() the direction is not merely observed, it is structural, and NEWS.md
now says so: predict.glmnet() returns beta[,left]*frac + beta[,right]*(1-frac),
and within the snapping tolerance frac is within a few ULP of 1, so a feature
carried only by the adjacent column leaks in at around 1e-17 and is selected on
abs(x) > 0. The interpolated support is a superset of the solved column's, so
snapping can only remove.

At css() it can go either way, and the review was right that I had not looked.
Measured over 400 seeds at n = 22, p = 14 with three named clusters and a weak
signal, comparing origin/main against this branch: five returned sets changed and
three were pure ADDITIONS. The mechanism is the documented tie rule -- a removal
lowers a cluster's selection proportion, that proportion meets a max_num_clusts
tie, and getSelectedClusters() resolves the tie by returning more clusters and
warning. The three addition seeds are exactly the three where the warning newly
fires, because the addition and the warning are the same event. Two consequences
are now disclosed: the warning fires on ordinary input more often than before
(11 runs against 9), and under options(warn = 2) cssSelect() stops with an error
at those three seeds where it previously returned.

An earlier attempt of mine failed to reproduce this and should not have been
trusted: it re-seeded immediately before cssSelect(), changing the RNG stream
from the sweep that found the divergence.

(2) CORRECTION RESIDUE. The previous commit fixed "the only one that can be" in
the narrative and left the identical claim nine lines below, inside the block
comment, where its own next-but-one sentence contradicts it. Now corrected there
too.

(3) A SCALAR GUARD. snapLambdaToGrid() gains stopifnot(length(s) == 1L). It is
about s rather than lambda_fitted, so it does not touch the documented
no-validation decision, and it converts the crossed-call hazard with the
same-arity sibling from a margin of one incidental assertion into a direct
failure.

(4) A RATE FIGURE DROPPED. Two sweeps measured roughly 1 in 554 and 1 in 176; the
PR body quoted the first as if it were the rate. The spread is the finding, so the
body now says "uncommon" and leaves each measurement where it was taken.

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

The narrow post-execution round and the sentinel's second post-implementation
pass both ran against 342f472. Neither blocked. Their findings are applied here
as one revision.

Two of the defects were introduced by 342f472 itself, which was the commit
answering a review about claims made without running them. That is the seventh
instance of this pattern on this branch and the fourth inside a commit correcting
the previous one, so it is recorded rather than quietly repaired.

THE GUARD'S COMMENT NAMED THE WRONG CROSSING. It said asserting a scalar s makes
the crossing that hands snapLambdaToGrid() the anchored site's arguments fail
directly. Measured, that crossing passes a scalar s, so the line never fires and
the suite output is byte-identical with and without it; that crossing is caught by
the two #199 red-green assertions. What the guard converts is the TRANSPOSED call,
where R's cryptic coercion error becomes a named precondition at a named line. The
comment now says so, and records that no legitimate caller can reach the guard at
all, since checkCssLassoInputs() rejects any lambda whose length is not 1 or 2.

THE GUARD HAD NO RED-GREEN COVER: deleting it reddened nothing, so it could have
been weakened later in silence. It is now pinned by an expect_error() in the
invariants block. fixed = TRUE is mandatory and verified rather than assumed --
grepl(m, m) on "length(s) == 1L is not TRUE" is FALSE because of the parentheses,
and at edition 2 a mismatched expect_error() aborts the render. Suite 4297 -> 4298;
the guard-deleted mutant now gives FAIL 1.

THE DIRECTION CLAIM'S REASON ARGUED ONLY HALF THE CASE. "Can only remove" needs
both that the blend's support is a superset of the solved column's and that a
feature the solved column carries survives the blend. Only the first was stated.
The second can fail by exact cancellation, which is why this is a well-tested
empirical claim -- 0 additions in 239,004 runs, 0 sign flips in 541,750 adjacent
column pairs -- rather than a theorem. NEWS.md now says both halves and says which
it is.

THE css() DISCLOSURE WAS INCOMPLETE IN THE WAY THAT MATTERS. selected_feats, which
is what cssPredict() consumes, can widen with NO warning at all and with
selected_clusts unchanged, through a within-cluster weighting tie rather than the
max_num_clusts one (seeds 336 and 390 of the recorded sweep). Tying the widening to
the warning would tell an options(warn = 2) reader they are covered when they are
not. NEWS.md now names both routes and says which is silent.

Also: a positional cross-reference added by 342f472 pointed at the wrong paragraph
and is replaced by the fact it was pointing at; "frac is within a few ULP of 1" is
19 ULP at the median and near 0 rather than 1 in some runs, so the wording no
longer carries a figure or an orientation.

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

Copy link
Copy Markdown
Owner Author

Follow-up at d46a431 — the four items landed; one surviving comment defect

Gate 0 errors | 0 warnings | 0 notes, CI 5/5, suite 182 blocks FAIL 0 | WARN 0 | PASS 4298. The new guard is genuinely pinned: deleting stopifnot(length(s) == 1L) aborts snapLambdaToGrid holds its invariants (#199).

The css() additions reproduce, and my earlier failure to see them was the artifact 342f472 diagnosed. My sweep re-seeded immediately before cssSelect(), changing the RNG stream. Removing that one line: 4 addition seeds in 400. The reviewer who measured this was right and I was wrong; the negative result I reported should not have carried weight.

d46a431's further disclosure is the one that matters most, and I confirmed it at seed 32: selected_feats — what cssPredict() consumes — widens through a within-cluster weighting tie with selected_clusts unchanged and no warning at all. My own sweep compared selected_clusts, so it could not have seen that route by construction. NEWS naming both routes, and saying which is silent, is the right fix.


The finding: index.Rmd:879 keeps an orientation d46a431 removed from NEWS

d46a431's message records that "frac is within a few ULP of 1" is wrong — 19 ULP at the median, and near 0 rather than 1 in some runs — "so the wording no longer carries a figure or an orientation." That was applied to NEWS.md. The cssLasso() call-site comment still reads:

# An ULP miss near the top of the path leaves frac a hair short of 1, and every coefficient the
# neighbouring column has and this one does not leaks in at the scale of that hair

Measured over 1,200 interpolated reads on real glmnet() paths, perturbing s by 2 ULP in each direction at path positions 2–5:

miss upward   (s > fitted entry): n=600  median frac = 0.000000
miss downward (s < fitted entry): n=600  median frac = 1.000000

The orientation is set entirely by which side the miss falls on. The sentence is correct for downward misses and exactly inverted for upward ones, and the qualifier "near the top of the path" does not rescue it — the split above is measured at positions 2–5.

The mechanism is unaffected, and the code is right. Whichever side the miss falls, snapLambdaToGrid() picks the nearer entry, which is the column carrying the weight indistinguishable from 1 — so the other column's exclusive features are the ones leaking in at ~1e-17, and the blend's support is a superset either way. Only the comment's orientation is wrong. NEWS.md already has this right, with "scaled by a weight indistinguishable from 1, or from 0, at double precision"; :879 is the copy that did not get the correction.

Suggested: drop the orientation the way NEWS did — "leaves frac indistinguishable from 1 or from 0 depending on which side the miss falls, and every coefficient the other column carries and this one does not leaks in at the scale of the complement."

By your own count in d46a431 this is the eighth instance of the branch's fix-the-quoted-copy pattern, and the fourth inside a commit correcting the previous one — this time inside the commit that recorded the pattern. Worth noting that the six other corrections in that commit left no residue; I grepped each.

Two corrections to my own review

  • The direction argument I gave was half an argument. I framed removal-only as structural with a negligible exception. d46a431 is right that it needs both halves — the blend's support being a superset, and a feature the solved column carries surviving the blend — and that the second fails on exact cancellation. Labelling it a well-tested empirical claim (0 additions in 239,004 runs, 0 sign flips in 541,750 adjacent pairs) rather than a proof is the more honest framing, and NEWS now does that.
  • I wrote "frac a hair short of 1" myself, propagating the same orientation error that survives at :879.

Two notes

  • The guard-deleted mutant gives error 1, not FAIL 1 as the commit message says: at edition 3 the mismatched expect_error() re-raises and aborts the block, taking 16 assertions with it (4298 → 4282). Caught either way — but the failed-versus-error distinction has hidden a real result on this repo before, so it is worth stating precisely.
  • In my stream the max_num_clusts warning fires less often on head (11 → 7), where 342f472's message reports more (11 → 9). Nothing shipped is wrong: NEWS.md says only that the warning "can fire where it did not before," which is a possibility claim and is correct. The frequency figure lives only in the commit message, and it appears to be stream-specific.

Ready to merge once :879 is fixed — one sentence, and it needs a rebuild.

Orchestrator-run verification at d46a431: gate, guard mutation, a 400-seed cssSelect() sweep without the re-seed, and a 1,200-read lambda.interp() orientation probe.

gregfaletto and others added 2 commits September 8, 2026 18:34
Review follow-up at d46a431. The cssLasso() call-site comment still said an ULP
miss "leaves frac a hair short of 1", an orientation d46a431 removed from NEWS.md
and did not remove here. Measured over 240 interpolated reads on real glmnet()
paths, perturbing s by 2 ULP in each direction at path positions 2-5:

    miss upward   (s above the fitted entry)   median frac = 0
    miss downward (s below the fitted entry)   median frac = 1

So the sentence was correct for downward misses and inverted for upward ones, and
"near the top of the path" did not rescue it -- the split is measured at positions
2-5. The mechanism and the code are unaffected: whichever side the miss falls,
snapLambdaToGrid() picks the nearer entry, which is the column carrying the weight
indistinguishable from 1, so the other column's exclusive features are the ones
leaking in and the blend's support is a superset either way. The comment now names
both sides with the measurement, and says not to write it as "a hair short of 1",
because that phrasing has propagated twice.

This is the eighth instance of this branch's fix-the-quoted-copy pattern, and it
survived five review rounds for a reason worth recording: every one of those rounds
was scoped to what the newest commit had changed, and this line was written in
c1ff287 and never re-touched, so no round had scope over it. The maintainer found it
by reading the whole file. An unscoped audit of the complete branch diff is running.

Also corrected, in the same class: the comment above the new expect_error() said
deleting stopifnot(length(s) == 1L) "reddens nothing in the suite", which described
the state before that assertion existed and reads as false now that it does.
Measured, the deletion records an ERROR rather than a failure -- failed 0, error
blocks 1, passed 4282 -- because the mismatched expect_error() re-raises and aborts
the block, taking the assertions below it. The comment says that, and says the
summary line's FAIL aggregates failures and errors, which is what made the earlier
report imprecise.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An audit of the COMPLETE branch diff -- unscoped by commit, unlike the five
earlier rounds -- checked 77 claims. Seven were wrong, and all seven sit in
material no earlier round had scope over:

    c1ff287, original implementation, never re-touched   3    never covered
    d46a431, the tip when the audit was briefed          2    not covered since
    the in-flight orientation fixes                      2    not yet covered
    the four intermediate correction commits             0    covered once each

Every claim in a commit a scoped round did cover held. The scoped rounds were not
careless; they were correct within their scope, and the defects lived in the gaps
between scopes. That is why this branch produced findings one at a time, and why
the maintainer found the last one by reading the whole file rather than a diff.

WHAT WAS WRONG, each re-derived before correcting:

The claim that the suite is byte-identical with and without the scalar guard went
stale the moment the same commit added an expect_error() for that guard -- the
identical failure to the orientation line, one commit later, written while fixing
that line's class of error.

The transposed call's error does not come from inside which.min(), which returns
an index quite happily; it comes from the && in the tolerance test. The length in
that message was quoted as 45 and matches no fixture here -- the paths in play are
61 and 13 -- so the figure is dropped rather than replaced.

"Transposed" was used for the crossed call sixty lines below the comment that
defines transposed as the argument-swapped one, which errors. The two are now
distinguished where both are named.

A crossed call was said to produce the per-s-slot union #190 warns about. It
returns a scalar, so predict.glmnet() yields one slot; #190's union comes from
calling predict.glmnet() with no s at all, which yields one slot per fitted
penalty.

The <= boundary was called one no reachable input sits on. The block's own
s = 0 fixture sits exactly on it, since |g - s| and the relative tolerance are
both 0 there. The mutant is undetectable for a different and checkable reason:
on that boundary both branches return the same double.

Also: "a median frac of 0" overstated by a few ULP and is now stated as
indistinguishable from 0; the 1e-17 figure was attached to the complement when it
belongs to the leaked coefficients; one over-long line rewrapped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.

1 participant