MILAB-6651: pin cellLinker side assignment in the TS linker engine - #1768
MILAB-6651: pin cellLinker side assignment in the TS linker engine#1768PaulNewling wants to merge 3 commits into
Conversation
Characterization tests for LinkerMap, mirroring the Rust LinkerIndex tests. They assert today's behaviour, which is wrong, so they must flip when sides become explicit. The real cellLinker is authored [sampleId, cellId <- sampleId, scClonotypeKey]. getAxesGroups returns groups ordered by smallest contained index and fromColumns destructures [left, right], so the sample/cell component becomes the one-side. Consequences, both asserted: - a clonotype-keyed source reaches cell-level axes (edges are stored many-side -> one-side), which is how cell-level columns end up in a clonotype-keyed p-frame; - a cell-keyed source cannot reach the clonotype at all. Authoring the same linker with the clonotype first reverses both. Also records the cross-engine parity expectation. Rust takes its component ordering from disjoint::DisjointSet::sets(); this file's getAxesGroups derives it from its own index scan. Two independent implementations that agree by coincidence rather than by contract, and they have already drifted twice. Keeping the two sides in sync is manual — a real shared fixture would need a generated contract. Note the trunk in these tests carries the whole parent tree: linker map keys come from getArrayFromAxisTree, so a cell trunk keyed on cellId alone misses for an unrelated reason.
Two problems with the tests added in 1ed1727. A test whose name overstated it. "parity: TS groups the real cellLinker the same way Rust splits it" asserted a strict subset of the test above it and performed no cross-engine check of any kind — the two repos share no fixture. Replaced with an exhaustive test that earns its place, and the parity rationale is kept as prose where it belongs. Assertions pinning order the source disclaims. getAxesGroups documents "There are no order inside every group" (linker_columns.ts:271), but the tests asserted group contents as ordered arrays. Now compared as sorted sets, so only the order *between* groups is pinned — which is the part side assignment actually reads. The replacement walks all 6 authoring orders and separates the two things MILAB-6651 turns on: membership is authoring-order independent, position is not. That is why no structural rule can recover the intended direction — it has to be declared or read from data. Mirrors split_component_order_follows_authoring_order_for_every_permutation on the pframes-rs side.
🦋 Changeset detectedLatest commit: c990c47 The changes in this PR will be included in the next version bump. This PR includes changesets to release 0 packagesWhen changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #1768 +/- ##
==========================================
- Coverage 52.90% 52.89% -0.01%
==========================================
Files 370 370
Lines 19994 19994
Branches 4435 4435
==========================================
- Hits 10578 10576 -2
- Misses 8110 8112 +2
Partials 1306 1306 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
check-changesets failed: the change touches a file under lib/model/common/src, so changesets counts the package as modified and requires a changeset. An empty one declares that no release is needed, which is accurate — the tests add no API, behaviour, or type change. Note the changeset has to be tracked by git. `changeset status --since` reads changesets from git rather than the working tree, so an untracked file is invisible to it and the check keeps failing.
What this does
Adds characterisation tests for
LinkerMap, mirroring tests added to pframes-rs. Test-only change.They assert the behaviour that exists today, which is wrong. They must flip when linker sides become explicit, and that flip is the proof the fix worked.
Why LinkerMap needs its own tests
Two engines implement the same linker convention. Rust
LinkerIndexplans queries inside pframes-rs. This TSLinkerMapcollects columns at model time —sdk/model/src/render/util/column_collection.ts:520andsdk/model/src/pframe_utils/axes.ts:77— where only specs are available andpl-model-commondeliberately carries no pframes-rs dependency.Both derive component ordering independently: Rust from
disjoint::DisjointSet::sets(), TS from its own index scan ingetAxesGroups. Nothing couples them, and they have already drifted twice — Rust hard-errors on more than two components while TS silently skips, and only Rust honoursexcludeColumns. So each side asserts the expectation separately. A genuine shared fixture would need a generated cross-repo contract.What the tests establish
pl7.app/sc/cellLinkeris authored[sampleId, cellId <- sampleId, scClonotypeKey].getAxesGroupsreturns groups ordered by smallest contained index, andfromColumnsdestructuresconst [left, right] = groups(linker_columns.ts:53), so the sample/cell component becomes the one-side. That is backwards: many cells map to one clonotype.Two consequences follow, both asserted:
linker_columns.ts:91), so inverted sides let a clonotype-keyed table traverse down to per-cell axes. This is how cell-level columns reach a clonotype-keyed p-frame.A fourth test walks all six authoring orders and separates the two things this issue turns on: group membership is authoring-order independent; group position is not. The axes group correctly however they are written down, and position alone decides which component is treated as the one-side. No structural rule can recover the intended direction from the spec — it must be declared or read from data.
Verification
vitest runinlib/model/common— 112 pass across 9 files.ts-builderformatter, linter, and type-check all clean.Two notes for a reviewer
The trunk in these tests carries its whole parent tree. Linker map keys come from
getArrayFromAxisTree, so a cell trunk's key is[cellId, sampleId]. PassingcellIdalone yields the key[cellId]and misses on key shape rather than on reachability. An early draft of these tests passed for exactly that wrong reason.Group contents are compared as sorted sets, deliberately.
getAxesGroupsdocuments "There are no order inside every group" (linker_columns.ts:271), so asserting a specific order within a group would pin something the source disclaims. Only order between groups is pinned.Separately: this package's
testscript passes--coveragebut instruments nothing — it reports 0% with 112 tests passing. Pre-existing, and not addressed here.