A patch release fixing one defect in viewer-store preparation, across all four surfaces (C++ core, Python, R, JS/WASM).
Action required for existing viewer stores
A viewer@0.1 store prepped by an earlier version may carry its count basis in the wrong orientation. Re-run extend_for_viewer() on such a store to repair it. lstar.validate() now reports the problem instead of passing silently, and the JS reader raises instead of returning wrong numbers.
Non-viewer stores, the on-disk format (spec_version 0.1), and every public signature are unchanged.
What was wrong
A viewer@0.1 store carries the count payload in both orientations, because the viewer has two hot paths and neither is a byte-range read away from the other:
- gene-major (
csc) — colouring an embedding by a gene reads one column; - cell-major (
csr,counts_cellmajor) — a cluster or lasso selection reads rows.
All three surfaces normalized the basis to CSC in memory only, for the kernels, and never wrote it back. The converter preserves whatever orientation the source had — and a raw AnnData .X is CSR in essentially every h5ad — so nobody owned the on-disk orientation. A store converted from h5ad and then prepped came out with two cell-major copies and no column-readable one: it paid the full 2× payload and delivered one of the two access patterns.
The JS reader's cscColumn then read a well-formed slice of the wrong axis, returning a cell's expression profile as a gene's column — plausible numbers, no error. Genes past nCells read empty locally, or 416 over HTTP.
What changed
extend_for_viewer()writes the normalized basis back (Python, R, JS) and stamps itprovenance.viewer="basis". An already gene-major basis is left untouched; in JS it is restamped metadata-only rather than rewriting a potentially huge array, and chunks the new layout no longer covers are pruned.validate()errors on a missing or non-csccount basis — symmetric with thecounts_cellmajorcheck it has always made.- The JS reader asserts orientation in
cscColumn/cscColumns/csrRow/csrRows, as the C++ core and Python's lazy reader always have. - The write layout keys its "raw, single-chunk" basis branch on the
cscencoding rather than the field name, so a cell-major measure can never be given a large uncompressed, un-range-readable layout. - Basis selection excludes the viewer's own cache-tagged navigators, so re-running the prep to repair a store cannot make it feed on its own output.
Why it was not caught
Three separate tests covered this axis and all passed, because they asserted invariance — "the prep gives the same navigators whether counts arrive CSR or CSC" — which is true, and strictly weaker than "the output basis is gene-major". A green run actively read as "CSR is fine". Meanwhile every fixture that was later read was built CSC, so no test ever composed "prep a store" with "range-read a gene column".
Both gaps are now closed, and docs/parity.md carries the rule ("Invariance is not correctness"). Also added: docs/releasing.md.
Install
pip install --upgrade lstar-sc