0095 the stats numerical layer publishes four members and no more - CyrilB1531/lodestar GitHub Wiki

0095 โ€” The numerical layer publishes what one caller needs, and no more

Status: accepted ยท Date: 2026-09-10

Context

Decision 0081 kept Lodestar.Stats' numerical layer internal and refused two ways of exposing it โ€” a public Lodestar.Stats.Special namespace, and a move into Lodestar.Abstractions. It also wrote the condition for changing its mind:

Publishing later stays possible the day a second package needs the same functions; unpublishing a public API once shipped does not.

#566 is that day. Lodestar.Stats.Regression ships an OLS table, and a table is a Student tail per coefficient, a Student quantile per confidence interval, and an F tail for the overall test. A separate package cannot reach an internal.

The same day arrives for Lodestar.Decomposition. A numerically sound least squares wants a QR of the design matrix, and this repository already writes one โ€” by hand, because decision 0059 refused Math.NET on freshness. The alternative to publishing it is a second copy of a delicate kernel.

Decision

Four members become public, and nothing else.

package published version
Lodestar.Stats Distributions.StudentSf, Distributions.StudentQuantile, Distributions.FisherSf 0.1.0 โ†’ 0.2.0
Lodestar.Decomposition QrDecomposition.Householder, with Q, R, RowCount, ColumnCount 0.1.1 โ†’ 0.2.0

RegularizedIncomplete, LogGamma, RegularizedP, RegularizedQ, Erfc, Normal.Sf, Normal.Quantile, the finite-sample Kolmogorov distribution, PartialPivotLu, JacobiSvd and the DenseBlock helpers stay internal. Nothing has asked for them, and 0081's asymmetry holds in both directions: publishing later is always available, unpublishing never is.

They do not go to Lodestar.Abstractions. 0081 refused that for this layer specifically; this record generalises the refusal โ€” Abstractions carries the types packages exchange, and a function is not a type two packages exchange. A shared implementation lives in the package that owns it, and its neighbour takes an edge.

What publishing cost, measured rather than predicted

0081 listed the price: a reference page each, a wiki-map entry, a use in the packaging sample, and "its own oracle corpus checked at the tolerance a general-purpose caller would need, not the one the ten tests above it happen to need." Paying it found two things.

The QR's corpus already existed. tests/oracles/decomposition_qr.json is replayed by the internal kernel's own tests, so the public wrapper owes shape and refusals, not numbers.

The Student quantile was not a quantile. Beta.StudentQuantile solves P(T > x) = p โ€” an inverse survival function โ€” and returns the opposite sign to scipy.stats.t.ppf. Internally nothing noticed, because its one caller wanted exactly that. Published under its own name it would have handed a caller -2.1788 where every printed table shows +2.1788, and a confidence interval built on it would have been reflected through its own estimate. Distributions.StudentQuantile publishes the conventional meaning and negates, which is the distribution's symmetry about zero rather than a correction; the internal helper is unchanged.

That is the concrete form of 0081's warning that each published member becomes "a parity promise in its own right". The new corpus, tests/oracles/stats_distributions.json, reaches 3.1e-24 and is compared relatively โ€” an absolute 1e-9 there accepts an implementation returning zero, which is what the internal tests' closed forms could never have caught.

Options refused

Publish the whole layer. Refused, as 0081 refused it: every member would become a promise nobody asked for, at a tolerance nobody stated.

Keep the layer internal and duplicate the four functions in the regression package. Refused: two incomplete betas in one repository disagree eventually, and the disagreement surfaces as two p-values for one statistic.

Publish StudentQuantile with the internal helper's sign, documenting the convention. Refused: a member named for a quantile that returns the inverse survival function is a trap a document cannot undo, and every reader who does not read the remark gets an interval the wrong way round.

What enforces it

The reference gate covers Lodestar.Stats through two directories now โ€” tests and distributions โ€” so a fifth published member without a page fails CI. check_sample_coverage.py requires a sample per public class in both packages, which are both on its enforced list.