Skip to content

build(deps): pin vortex to a patched fork for the #468 and #472 determinism fixes - #481

Merged
gerchowl merged 7 commits into
devfrom
build/475-pin-vortex-fork
Sep 29, 2026
Merged

gerchowl merged 7 commits into
devfrom
build/475-pin-vortex-fork

Conversation

@gerchowl

@gerchowl gerchowl commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #474 (which adds the test this PR flips). Targeting #474's branch rather than dev so the
diff is only this change; GitHub retargets when #474 merges. Stacked PRs get CI now thanks to #473.

Why

vortex 0.75.0 has two defects that are the same underlying bug class: a write-time statistic
computation leaking into the serialised bytes, so a sealed content_hash stops being a function of
the data alone.

what leaks axis effect
#468 the debug_assertions is_sorted assert calls cache_is_sorted, which sets Stat::IsSorted on the patch indices build profile debug wrote 24 bytes more than release
#472 sum_float_all skips NaN, so +inf and -inf meet and evaluate inf + -inf; Stat::Sum is persisted architecture x86_64 0xfff8… vs aarch64 0x7ff8…

Both fixed on a fork of the 0.75.0 tag, pinned here by full commit SHA (never a branch name — a
branch would let pinned content move under an unchanged-looking lockfile). #480 tracks removal.

Two requirements that fail silently

Worth reading before reviewing, because each one produces a confident, meaningless "nothing changed":

  1. All 27 vortex crates must be patched together. Patching vortex-array alone leaves its
    siblings resolving from crates.io → two copies of vortex-proto/vortex-buffer and ~1000 type
    errors.
  2. The 0.75.0 tag ships placeholder version 0.1.0 (the real version is injected at publish).
    Without a version-setting commit on the fork branch, cargo prints patch ... was not used in the crate graph and quietly keeps using crates.io.

So the pin was verified positively, not by the absence of a warning: on x86_64 the sealed float
bytes no longer contain fff8000000000000, now contain 7ff8000000000000, and became byte-identical
to what unpatched aarch64 produced
.

#474's known-limitation test, flipped

known_limitation_468_* asserted the bug was still present, so it fails once fixed — by design.
It becomes full_span_int_container_bytes_are_build_config_independent, with one expected length
per shape and no cfg!(debug_assertions) branch. That absence is the assertion: flake.nix runs
it under both cargo test and cargo test --release, so a re-divergence fails in exactly one line.

Measured both profiles on the pinned fork before rewriting the expectations, as requested:

shape unpatched debug unpatched release pinned (both)
bare [i64::MIN, 0, i64::MAX] 2796 2772 2772
same via table::encode 2940 2916 2916
control [i64::MIN, i64::MAX] 2444 2444 2444

Both profiles agree on the former release numbers — the debug build was the one carrying the extra
24 bytes of cached statistic, which is the expected direction.

Gate B: every line explained

458 insertions against 458 deletions across 8 snapshots — perfectly balanced. Every changed line
is identical apart from gaining the (https://github.com/gerchowl/vortex?rev=…) source tag, verified
by re-comparing with that tag stripped:

arrow-array.txt       OK  (57 lines gained only the git source tag)
arrow-buffer.txt      OK  (100)        arrow-schema.txt   OK  (149)
pco.txt               OK  (7)          vortex-array.txt   OK  (50)
vortex-btrblocks.txt  OK  (4)          vortex-buffer.txt  OK  (90)
vortex-file.txt       OK  (1)
blake3.txt / zarrs.txt / zstd.txt      unchanged
VERDICT: source annotation ONLY — no feature changed

No seal-path crate gains or loses a feature, so no codec resolves differently.

Verification

check result
crane vendoring of the git dep hermetic, no outputHashes needed — verified by building checks.x86_64-linux.feature-snapshots, which vendored cargo-git-https-github.com-gerchowl-vortex and reached the Gate B diff
gen_corpus vs committed corpus/corpus.json byte-identical — no golden moved on this branch (the ingest corpus is not here; its float fixture does move, see the comment below)
gen_corpus dev vs release identical (not guaranteed before the fix)
conformance tests 3/3 pass
flipped #468 guard, both profiles pass
cargo deny check sources sources ok
DECODE_PATH_CRATES locked versions all 10 unchanged → the ingest_decoder digest cannot move

cargo-deny tightened rather than left permissive: unknown-git goes from allow to deny, with
allow-git naming exactly the pinned repository. A blanket allow would let a future stray git
dependency in unnoticed, which is precisely the review signal a fork pin should raise.

Upstream — the exit path

Both defects are filed and fixed upstream, so this pin has a visible end:

defect upstream issue upstream PR
#468 side-effecting is_sorted assert vortex-data/vortex#10119 vortex-data/vortex#10121
#472 platform-dependent NaN Sum vortex-data/vortex#10120 vortex-data/vortex#10122

Both were rebased onto vortex's develop, not the 0.75.0 tag: 23 commits had touched those files
since, including an AggregateVTable rework and a new sum_v2 that finalises floats through its own
path, so #10122 fixes two sites there. Both defects were re-verified as still present on develop
before filing.
The tessera pin stays on the 0.75.0-based fork branch and is unaffected by that
rebase.

#480 tracks removing the pin, with the checklist.

Notes for reviewers

  • Crane's vendoring emits cp: cannot stat 'README.md' per vortex crate. Cosmetic: the workspace
    include key lists a per-crate README that does not exist in the git checkout. No compiled input is
    affected, and cargo tree over the vendored deps works.
  • parquet is absent from this branch's lockfile (the ingest lane arrives with feat(ingest): generic table ingest — Parquet, Arrow IPC and CSV (ADR-0056 P1) #460), so its version
    is trivially unchanged here. tsra-386 is regenerating the ingest corpus under both profiles on
    this branch — expected movement only in the patch-producing int and NaN-sum float fixtures.
  • The fork branch is gerchowl/vortex:tessera/0.75.0-patched: 0.75.0 + the two signed fixes + one
    non-upstream version commit. It is branch-protected (locked, no force-push, no deletion,
    enforce_admins) so the pinned commit cannot be GC'd out from under a fresh build.

Refs #468, #472, #474, #480

…hid on

vortex 0.75.0 writes different container bytes for an integer column whose encoding
produces patches, depending on whether `debug_assertions` is enabled. `[i64::MIN, 0, MAX]`
is the minimal case: 2796 bytes with them on, 2772 with them off, a constant 24-byte delta.
The compression is identical in both — same chosen schemes (`zigzag -> bitpacking`), same
`compressed_nbytes`, same estimated and achieved ratios — so the difference is in the file
metadata written around the values.

The cause is an **assertion with a side effect**. `vortex-array/src/patches.rs` guards patch
construction with a `#[cfg(debug_assertions)]` `is_sorted(&indices, …)` check; evaluating
that aggregate executes and annotates the indices array, and the written container reflects
it. Measured, not reasoned: toggling `debug-assertions` alone flips the bytes in both
directions, while `overflow-checks` and the optimisation level change nothing — so this is
neither UB nor codegen. Only patch-producing inputs diverge: `[i64::MIN, i64::MAX]`, which is
byte-identical across profiles, produces no patch arrays at all.

Three additions, and the middle one is the point:

- `known_limitation_468_full_span_int_container_bytes` reproduces it through **vortex APIs
  only**, with the stock `WriteStrategyBuilder::default()` — no tessera types, no
  `exclude_schemes`, no Pco — so it cannot be mistaken for something our compressor
  configuration causes. It asserts the bug is STILL PRESENT and therefore **fails when
  upstream fixes it**, which is the signal to restore the `i64` extremes that #460's fixture
  currently gives up. A `#[ignore]` would let the fix land unnoticed.
- `seal-profile-determinism` regenerates the conformance corpus under BOTH profiles and
  requires them to agree with each other and with the committed file. Worth stating plainly:
  the committed `corpus/corpus.json` was produced by a bare `cargo run` (dev profile) while
  `workspace-test` runs the same code release-built, so the two halves of our own CI were
  checking different builds and nothing compared them.
- ADR-0057 §5 records the lesson, which is sharper than "an exclusion list is not a proof":
  **a determinism argument about the codec does not cover the container.** The ALP/ALPRD
  exclusion's reasoning is sound and is about the encoding of *values*; this defect has
  identical codec decisions and differs in the metadata around them, and the seal covers the
  whole container.

Blast radius, stated honestly: the axis is `debug_assertions`, so every release build agrees
with every other release build on any architecture, and shipped binaries are mutually
consistent. What diverges is a debug-built tool against a release-built one. It is still an
S15 violation, because "the bytes are a function of the data" has to hold for *a* build of a
version, not only for the release ones.

`seal-profile-determinism` is verified to evaluate (`nix eval …drvPath`) but NOT built
locally: `/` is at 97%, and a two-profile derivation in one sandbox is what exhausted it
before. CI has its own disk.

Refs: #468
The third determinism lesson from the same sitting, and the one most likely to recur, so it
belongs next to the other two in ADR-0057 §5 rather than only in the commit that fixed it.

The `.tsra` STORED invariant — what makes a block's bytes fetchable by range, and therefore
what makes a cloud reader possible at all — was documented and written but never *checked* on
read. It did not need to be: the workspace pinned `zip = { default-features = false }`, so no
decompressor existed and a compressed member simply failed to decode. Adding `deflate-flate2`
for the `.npz` reader removes that accident workspace-wide, because cargo unifies features, and
a deflated `.tsra` would then have **verified perfectly** while silently ceasing to be
range-readable.

Stated as a rule rather than a war story: an invariant that holds because some capability is
*missing* stops holding the moment anyone adds that capability for an unrelated reason, and
nothing in the diff will mention it.

Also records what the two determinism defects have in common, since it explains why the
existing exclusion list could not have caught either: #468 and #472 both diverge in the metadata
*around* the values — a container record and a persisted `Stat::Sum` — while the encoded values
are identical. `deterministic_table_compressor` reasons about the codec; the seal covers the
whole container.

Refs: #468
Keeping this PR current. `dev` brought #445 (devkit 1.17.0), #473 (the #463 fix — stacked PRs
now get the PR checks), #470, #465 (the encode-trace callsite race), #464, #469 and #471. Clean
merge; only `flake.nix` needed auto-merging, alongside the `seal-profile-determinism` check this
branch adds.

#465 touches `tessera-io`, which is where this branch's reproducer lives, so the thing worth
checking was whether the #468 divergence still behaves: `known_limitation_468_full_span_int_container_bytes`
still passes under **both** profiles on the merge, so the known byte counts are unchanged and the
test still fails-when-fixed as intended.

Refs: #468
…claim to this defect

Review found two statements that read as broader than the evidence, both of which would have
merged onto `dev` as claims this PR cannot support.

THE RELEASE-BUILD CLAIM CONTRADICTED ITS OWN PARAGRAPH. §5 said "every release build agrees with
every other release build, on any architecture" — three paragraphs above a note that #472 is a
**same-profile** divergence between x86-64 and aarch64. Both cannot be true. The claim is really
about #468's axis only, so it now says so explicitly, and says outright that it is not a general
statement, naming #472 as the counter-example. Two release binaries really can disagree; just not
because of this defect.

THE STORED CHECK IS NOT IN THIS PR. The "guarantee that depends on an absence" paragraph said the
invariant is "enforced now", but the check lands with the `.npz` lane in #461 — this branch is
based on `dev` and adds no `zip` feature, so on `dev` at this PR's merge point nothing is enforced
yet. Reworded to attribute it to #461, which is where the decompressor and the replacement arrive
together. The reproducer's own message and doc comment had the same problem from the other
direction: they pointed at `tessera_ingest::corpus::write_scalars`, a function that exists only on
the ingest branches, so a reader on `dev` would be sent to something that is not there. Both now
describe the fixture neutrally and stay true wherever they are read.

TWO SCOPE NOTES, so the gate is not read as doing more than it does:

- `tessera-io`'s conformance corpus contains no integer column that produces bitpacking
  **patches**, so `seal-profile-determinism` does not currently reproduce #468. It guards the
  class going forward; the specific shape is pinned by the reproducer. §5 now says this, along
  with what would change it (a patch-producing fixture, once upstream is fixed and the expected
  bytes stop depending on the build).
- The check's cost is documented: `cargoArtifacts` pre-builds the **release** dependency graph, so
  its dev half compiles vortex from source every run, which is the bulk of the leg. A second
  dev-profile `buildDepsOnly` is not cleanly expressible — a derivation inherits one
  `cargoArtifacts` — so it is left as a stated cost with the cheaper fallback named (release-only
  on PRs, both profiles on `main`) rather than a speculative nix refactor.

Refs: #468
…minism fixes

vortex 0.75.0 has two defects that both leak a WRITE-TIME STATISTIC into the
serialised bytes, so a sealed `content_hash` stops being a function of the data
alone:

  #468  the `debug_assertions`-gated `is_sorted` assert on patch indices calls
        `cache_is_sorted`, which SETS `Stat::IsSorted` on them. Cached stats are
        serialised, so a debug-built writer emitted 24 bytes more than a
        release-built one.
  #472  `sum_float_all` skips NaN, so a column with both `+inf` and `-inf`
        evaluates `inf + -inf`. IEEE leaves that NaN's bits unspecified and the
        targets disagree (x86_64 `0xfff8…`, aarch64 `0x7ff8…`); `Stat::Sum` is
        persisted, so the bytes depended on the writing host.

Both are fixed on a fork of the 0.75.0 tag and pinned here by FULL COMMIT SHA
until a release carries them. #480 tracks removing the pin.

Two non-obvious requirements, both of which fail SILENTLY:

  - all 27 vortex crates must be patched together. Patching `vortex-array`
    alone leaves its siblings on crates.io, giving two copies of
    `vortex-proto`/`vortex-buffer` and ~1000 type errors.
  - the 0.75.0 tag ships the placeholder version 0.1.0 (the real one is injected
    at publish), so without a version-setting commit on the fork branch cargo
    reports `patch ... was not used in the crate graph` and keeps using
    crates.io. An ignored patch looks exactly like a patch that changed nothing,
    so the pin was verified POSITIVELY: on x86_64 the sealed float bytes no
    longer contain fff8000000000000, now contain 7ff8000000000000, and became
    identical to what unpatched aarch64 produced.

#474's `known_limitation_468_*` asserted the bug was still present, so it fails
once fixed — by design. It is flipped here into the positive guard:
`full_span_int_container_bytes_are_build_config_independent`, with ONE expected
length per shape and no `cfg!(debug_assertions)` branch. That absence IS the
assertion; flake.nix runs it under both profiles, so a re-divergence fails in
exactly one of the two lines. Both profiles agree on the former release numbers
(bare 2772, sealed 2916) — the debug build was the one carrying the extra 24
bytes.

Gate B moves, and only in one way: 458 insertions against 458 deletions across 8
snapshots, every changed line identical apart from gaining the
`(https://github.com/gerchowl/vortex?rev=…)` source tag. No feature changed
anywhere — verified by re-comparing with that tag stripped. blake3, zarrs and
zstd are untouched.

cargo-deny `[sources]` is tightened rather than left permissive: `unknown-git`
goes from `allow` to `deny` with `allow-git` naming exactly the pinned
repository, so a future stray git dependency is a review event instead of
silently fine.

Verified: crane vendors the git dependency hermetically with no `outputHashes`
needed; `gen_corpus` output is byte-identical to the committed corpus AND agrees
between dev and release; all 3 conformance tests pass; every
`DECODE_PATH_CRATES` version is unchanged, so the `ingest_decoder` digest cannot
move.

Refs: #468, #472, #474, #480
@gerchowl

Copy link
Copy Markdown
Contributor Author

Scope correction on "no golden moved", plus the ingest-lane verification

Clarifying my own claim before it misleads anyone. The table above says "no golden moved", and that
is accurate for this branch — corpus/corpus.json, the sealed corpus, is byte-identical. It is not
true of the ingest corpus, which is not on this branch. One ingest golden does move, by design.

tsra-386 regenerated ingest-corpus.json under both profiles against this pin, on a deliberately fresh
target dir:

PROFILE-STABLE under the pin: dev == release        <- the #468 fix, confirmed on the ingest lane

ingest_parquet_floats     2ee5bf4d1b33 -> 33b065510f5e   <== the ONLY content_hash that moved
every other fixture (13)                unchanged
ingest_decoder triples that changed:    NONE
manifest_hash moved:                    only ingest_parquet_floats

Two things make that the right movement rather than a surprise:

The regenerated ingest corpus is not carried here — this branch has no ingest corpus to update. It
belongs on #460/#461 after this merges.

ingest_decoder confirmed empirically, not just by code reading

My claim above was from reading DECODE_PATH_CRATES and comparing locked versions. tsra-386 confirmed
it end to end: every ingest_decoder triple is byte-identical
({arrow-rs/parquet, =58.3.0, blake3:606b736dd205…}), and independently, manifest_hash moved for
only the one fixture whose content moved — had the decoder digest shifted, every fixture's
manifest_hash would have moved with it.

Two traps in reusing this branch's manifests

Worth recording for anyone combining this with #460: this branch's Cargo.lock has no parquet, and
its Cargo.toml has no arrow-array in workspace.dependencies
— both arrive with #460. Taking
either file wholesale onto the ingest branch would remove the ingest lane's dependencies while
appearing to test it. The correct manoeuvre is to keep the ingest branch's manifest and lockfile and
append only the [patch.crates-io] block, letting cargo resolve. Done that way, vortex-array moves to
the git rev and parquet stays at 58.3.0 from the registry — so the pin does not perturb parquet,
confirmed rather than assumed.

A weakness this manoeuvre makes reachable

Filed as the second finding on #477: lock_version in tessera-ingest/build.rs parses only
version = "…" and ignores source, so a same-version git pin of a decode-path crate would be
invisible to the sealed ingest_decoder digest. It does not bite here — vortex is the encoder and is
correctly absent from DECODE_PATH_CRATES — but the pattern this PR establishes makes it reachable:
forking parquet at 58.3.0 to fix a decode bug would leave every product claiming the unpatched
decoder. This is the same phenomenon as the Gate B source-tag churn, seen from the other side: a git
source changes the provenance without changing the version string.

Upstream

Both fixes are now filed against vortex with issues and PRs, so the pin has a visible exit:

defect issue PR
#468 vortex-data/vortex#10119 vortex-data/vortex#10121
#472 vortex-data/vortex#10120 vortex-data/vortex#10122

Both were rebased onto vortex's develop rather than the 0.75.0 tag — 23 commits had touched those
files since, including an AggregateVTable rework and a new sum_v2 that has its own float finalisation
site, so #10122 fixes two sites there. Both defects were re-verified as still present on develop
before filing. The tessera pin itself stays on the 0.75.0-based fork branch, which is unaffected.

…via the ingest golden

#472's fix had no direct guard here: the only thing that would have caught an
early pin removal was #460's ingest corpus, on another branch. Dropping the pin
before the upstream fix ships would have gone unnoticed in this crate.

Seals an f64 column holding both `+inf` and `-inf` — the shape that makes
vortex's `Sum` statistic evaluate `inf + -inf` — and asserts the sealed bytes
carry the canonical quiet NaN and NOT x86_64's `0xfff8_0000_0000_0000`. Added to
the same flake.nix both-profiles gate as the #468 guard.

Proven to bite: it FAILS with the `[patch.crates-io]` block removed and passes
with it, so it is a real guard and not a tautology.

Two honest limits, both stated on the test:

  - it cannot fire on aarch64, which always produced the canonical value, so the
    sealed bytes there are identical with and without the fix. The guard bites on
    x86_64 — the host whose bytes were wrong. A green aarch64 leg is not evidence
    against a dropped pin.
  - it asserts byte PATTERNS rather than a digest, following the #468 guard's
    reasoning: a vortex bump legitimately moves bytes, so the test should fire for
    a determinism regression and not for every dependency update.

Refs: #472, #480
@gerchowl
gerchowl changed the base branch from fix/468-int-full-span-container-determinism to dev September 29, 2026 01:02
#474 landed as a squash (38c9efa), so this branch's copies of its original
commits conflicted with it. Resolved by taking this branch's versions of
flake.nix and tessera/crates/tessera-io/src/table.rs: this branch is a strict
superset of #474's content there — verified by diffing both files against
origin/dev, whose only differences were exactly the two intended changes (the
#468 test flipped to `full_span_int_container_bytes_are_build_config_independent`
and the new #472 canonical-NaN guard, plus the matching flake.nix invocations).
No dev-only content exists in either conflicted region, so nothing from the
squash is dropped.

Refs: #481
@gerchowl
gerchowl merged commit e73449b into dev Sep 29, 2026
19 of 21 checks passed
gerchowl added a commit that referenced this pull request Sep 29, 2026
… are fixed

`dev` now pins vortex to a patched fork (#481), which fixes both determinism defects this
branch was waiting on. What that does to the corpus is the substance of this commit, so it is
worth stating as a result rather than a claim.

**One fixture moves, and to a value predicted in advance.** `ingest_parquet_floats`:
`2ee5bf4d1b33…` → `33b065510f5e…`. That second value is not new — it is what **aarch64 was
already producing** before the pin, taken from this PR's own failing aarch64 log. #472 was a
persisted `Stat::Sum` NaN: `sum_float_all` skips NaN, so a column holding `+inf` and `-inf`
summed to a NaN whose sign bit differed by architecture (x86-64 `0xfff8…`, aarch64 `0x7ff8…`),
one bit. Canonicalising to `0x7ff8_0000_0000_0000` therefore has to leave aarch64 untouched and
move x86-64 onto it — so predicting the value beforehand and hitting it distinguishes "the fix
changed the bytes" from "the fix made x86 agree with ARM", and only the second is consistent with
that diagnosis. Everything else is byte-identical and no `ingest_decoder` triple moved.

**And the corpus is now profile-independent**, which is #468's half: the regeneration was written
only after confirming the dev-profile and release-profile outputs are byte-identical, the property
ADR-0057 §5's `seal-profile-determinism` check exists to enforce and the one #468 made false.
`ingest_parquet_scalars` does **not** move, because it was narrowed to ±(2^53+1) earlier on this
branch — profile-stable by construction, so the pin has nothing to change there.

GATE B — the three conflicted snapshots were **regenerated, not hand-merged.** Both sides
legitimately changed them (the pin adds the fork's source tags; this branch adds the `parquet`
node's edges), and a generated file that has been hand-merged looks authoritative without being
generated. Verified afterwards rather than assumed: no **pre-existing** third-party crate gains a
feature — every gain belongs to the new `parquet` node — nothing is lost, and both watched
canaries are unchanged (`vortex-btrblocks/alp` still absent, which is the one that would silently
re-encode every float column; `arrow-array/chrono-tz` unchanged).

ADR-0057 §5 is rewritten for the post-pin state: both defects fixed via the fork, with upstream
`vortex-data/vortex` #10119 / PR #10121 and #10120 / PR #10122, and #480 tracking removal of the
pin; the agreed byte counts under the pin (2772 bare, 2916 sealed, in **both** profiles — the
debug build was the one carrying the extra 24 bytes of cached statistic); #472's one-bit cause
recorded beside #468's, since the pair is what the section argues from; and the note that a
patch-producing integer fixture is now **safe** to add to `tessera-io`'s corpus, because the
expected bytes no longer depend on the build — worth doing while #480 is open, as it is exactly
what would catch a regression when the pin is dropped.

The reproducer that guarded the shape has been inverted upstream of here (#481): it asserted the
divergence was *still present*, so it failed the moment the fix landed. That failure is what
forced this section to be rewritten instead of going quietly stale.

One more thing the merge surfaced, which is a gate earning its keep: `dev` brought a **generated**
schema reference (#484) that is asserted against the live registry, and this branch adds the
`table` and `array` builtin schemas — so the committed copy went stale the instant the merge
happened, and `schema_reference_matches_the_committed_copy` failed. Regenerated: +28 lines
documenting exactly those two schemas, nothing removed. Neither branch could have caught it alone,
which is the argument for asserting derived docs against their source rather than reviewing them.

Refs: #386
gerchowl added a commit that referenced this pull request Sep 29, 2026
`nix flake check` stops at the first failing derivation, so on #481 an unrelated
`minio` substitution failure took down all 17 aarch64 checks: the leg went red
with every check reporting `(cancelled)` and nothing naming a real failure. The
determinism gates, clippy and the wasm checks do not depend on minio at all. Two
reviewers independently had to read the job log to establish that the pin under
test was not implicated — the cost a signal imposes when a cancelled check and a
failed check look identical from the PR list.

Three changes, one per problem.

`--keep-going`: a failure no longer cancels the other checks, so every check
reaches a verdict and the failing one is named.

`--fallback`: a derivation that cannot be substituted is built from source rather
than failing the run. Measured on these exact runner images for the derivation
that actually bit us:

    minio, from source     x86_64  102s      aarch64  73s
    minio, substituted     x86_64   13s      aarch64  80s

On aarch64 building it is FASTER than pulling its 36 MiB from cache.nixos.org —
the download is the slow part, not the Go build. Against the leg's existing
90-minute cap, noise. The flag applies to every derivation, so a pathological
outage on something large could build until that cap instead of failing in
seconds; accepted deliberately, because both outcomes are a red leg, the budget
already bounds it, and the common case stops being a spurious failure.

A per-check summary table, derived from the STORE rather than by parsing nix's
human output: a check whose output path is valid was built and so passed.
`--keep-going` is what makes that inference sound — every check is attempted, so
a missing output means that check failed rather than that it was skipped. Both
directions were verified locally (a built check reads as passed, an unbuilt one as
not built) rather than assumed.

`nix flake check` stays the command. A per-check `nix build` loop would give
exact verdicts and keep-going for free, but this file's header argues that the
same command running on a developer's machine and in CI is what prevents
"passes locally / fails in CI" drift, and that property is worth more than tidier
reporting.

Also documents, in flake.nix at the point the decision is made, why the
minio-range-read check strips minio's `knownVulnerabilities`: a sandboxed,
network-less, throwaway test fixture, reached only over loopback by the test
itself, where the alternative is having no test of the cloud range-read path
against a real object store. `meta` is not part of the derivation, so the
override changes no output hash.

Refs: #483
gerchowl added a commit that referenced this pull request Sep 29, 2026
…C|CSV> (#460)

The table lane of ADR-0056: Parquet, Arrow IPC and CSV (explicit schema only, locale-independent parsing) normalised into Tessera table primitives, with nested-nullability composed at every level, the ingest_decoder recipe triple sealed per ADR-0056 §6a (hand-written triples rejected for every backend), the §7 laundering rule, new sealed fields (Column.referencing, Column.sensitivity incl. Unknown, manifest ingest_transform) with the existing corpus byte-identical, a three-producer (pyarrow/polars/DuckDB → one content_hash) nix check, and the ADR-0056 §12a amendment. The ADR-0057 §5 note is updated for the vortex pin (#481).

Refs: #386
gerchowl added a commit that referenced this pull request Oct 5, 2026
…543)

Refs #513 — clears Dependabot **#42, #43 and #44**. The fourth
(`thrift`, #45) is blocked upstream; evidence at the bottom.

## Why a manual bump was needed

`dicom-transfer-syntax-registry` 0.9.1 declares `jxl-oxide = "^0.10.2"`,
and the patched 0.12.6 is outside
that range, so neither bot could fix it in place. 0.10.0 requires
`^0.12.6`, so the family moves together:

| crate | before | after | fixed in | advisory |
|---|---|---|---|---|
| `jxl-grid` | 0.5.3 | **0.6.2** | 0.6.2 | GHSA-5pmv-rx8r-wmv5 (high) |
| `jxl-oxide` | 0.10.2 | **0.12.6** | 0.12.6 | GHSA-66m8-c62j-h6v5 |
| `jxl-modular` | 0.9.1 | **0.11.3** | 0.11.3 | GHSA-2v8p-fqpx-2q3w |

24 lockfile versions move in total (ten `dicom-*`, twelve `jxl-*`, and
`snafu` 0.8.9 → 0.9.2 which dicom
0.10 requires); one package added (`jxl-jbr`), none removed. **No
workspace source change was needed** — the
0.9 → 0.10 API break does not reach our call sites.

Context worth keeping: we do not enable `jpegxl`, so the jxl crates are
**lockfile-only** and were never in
the build graph. The advisories were real as lockfile facts and inert as
runtime ones — hence `chore`, not
`fix`.

## No sealed byte moves — run, not assumed

- **The ingest corpus regenerates byte-identical** to the committed
goldens: no `content_hash` and no
`manifest_hash` moved. Expected, since the ADR-0056 §6a decoder digest
covers the *generic* lanes and dicom
is a vendor backend outside it — but the DICOM lane carries its own
golden hashes (#207), and a pixel-decode
  change would have moved those. **15 DICOM tests pass.**
- **Gate B feature snapshots regenerate unchanged**
(`scripts/feature-snapshots.sh`, zero files differ).
Nothing dicom pulls sits on the sealed byte path those snapshots watch.
- Full workspace tests pass under `--locked`; clippy clean; prek clean;
commit signed.

The one deliberate snapshot edit is `info.trycmd`'s human `backends:`
line, which pins resolved backend
versions on purpose:

```diff
-backends:   … dicom 0.9.1 · dicom-series 0.9.1 · …
+backends:   … dicom 0.10.0 · dicom-series 0.10.0 · …
```

Hand-edited as the single literal that changed — the JSON block already
uses `[..]` for versions. Deliberately
**not** `TRYCMD=overwrite`, which records whatever the binary printed
and cannot distinguish an intended value
from a regression.

## Why `thrift` is not here: it is blocked on Vortex, not on us

#513 asks for `parquet`/`arrow-*` ≥ 59.0.0, since parquet 59 drops
`thrift` entirely. That is not currently
possible:

```
vortex-array  0.75.0 requires arrow-array ^58.3
vortex-buffer 0.75.0 requires arrow-buffer ^58.3
vortex-error  0.75.0 requires arrow-schema ^58.3
vortex-layout 0.75.0 requires arrow-array ^58.3
vortex-runend 0.75.0 requires arrow-array ^58.3
```

`^58.3` means `< 59.0.0`. So moving our `=58.3.0` pins to 59+ would
**not upgrade arrow — it would resolve a
second arrow tree**, which is precisely what the pin comment says those
pins exist to prevent:

> *"58.3.0 is the version Vortex already resolves, so `arrow-array` /
`-buffer` / `-schema` are ALREADY in the
> default graph (Gate B snapshots all three) and these pins add no
second arrow copy."*

Two arrow versions either side of the `tessera-ingest` → `tessera-io`
boundary is an ADR-0056 hazard surface,
not a tidiness question. And `thrift` is a **non-optional** dependency
of parquet 58.3.0, so no feature
selection drops it either.

So #45 needs Vortex on arrow ≥ 59 first — either upstream, or carried in
the `gerchowl/vortex` fork we already
pin for #468/#472 (#481), which is a materially bigger change than a
version bump and wants its own decision.
Left open and reported rather than forced.
gerchowl added a commit that referenced this pull request Oct 19, 2026
## 🤖 New release

* `tessera-core`: 0.1.0-alpha.1 -> 0.1.0-alpha.2
* `tessera-io`: 0.1.0-alpha.1 -> 0.1.0-alpha.2
* `tessera-ingest`: 0.1.0-alpha.1 -> 0.1.0-alpha.2
* `tessera-cli`: 0.1.0-alpha.1 -> 0.1.0-alpha.2
* `tessera-py`: 0.1.0-alpha.1 -> 0.1.0-alpha.2
* `tessera-wasm`: 0.1.0-alpha.1 -> 0.1.0-alpha.2

<details><summary><i><b>Changelog</b></i></summary><p>

## `tessera-core`

<blockquote>

##
[0.1.0-alpha.2](tessera-core-v0.1.0-alpha.1...tessera-core-v0.1.0-alpha.2)
- 2026-10-19

### Added

- *(cli,core)* tessera provenance walk and inspect --resolve-config
([#490](#490))
- *(ingest)* generic table ingest — tessera ingest table
<PARQUET|IPC|CSV> ([#460](#460))

### Fixed

- *(core,cli)* compute array statistics in exact integer arithmetic

### Other

- ingest cookbook, generated product-schema reference, tested Python
write example, cli.md stubs
([#484](#484))
</blockquote>

## `tessera-io`

<blockquote>

##
[0.1.0-alpha.2](tessera-io-v0.1.0-alpha.1...tessera-io-v0.1.0-alpha.2)
- 2026-10-19

### Added

- *(cli)* serve array stats from the chunk-index with no decode, and add
stats --json
- *(ingest)* generic array ingest — NumPy .npy and .npz (ADR-0056 P1)
- *(cli,core)* tessera provenance walk and inspect --resolve-config
([#490](#490))

### Fixed

- *(io)* chunk statistics on nullable columns with nulls
- *(deps)* upgrade rustls and zip past
RUSTSEC-2026-0346..0349/0367..0369
- *(cli,io)* pin inspect --json, guard stage-dir downgrade resume, route
verify through the resource cap, 5.5x single-column reads
([#479](#479))
- *(io)* gate seal determinism across build profiles and pin the #468
vortex limitation ([#474](#474))
- *(io)* close the encode-trace callsite race under CPU contention
([#465](#465))
- *(ingest)* seal the ADR-0058 recipe on the streaming path and surface
it in inspect ([#453](#453))

### Other

- *(deps)* pin vortex to a patched fork for #468/#472
([#481](#481))
</blockquote>

## `tessera-ingest`

<blockquote>

##
[0.1.0-alpha.2](tessera-ingest-v0.1.0-alpha.1...tessera-ingest-v0.1.0-alpha.2)
- 2026-10-19

### Added

- *(ingest)* generic array ingest — NumPy .npy and .npz (ADR-0056 P1)
- *(ingest)* generic table ingest — tessera ingest table
<PARQUET|IPC|CSV> ([#460](#460))

### Fixed

- *(deps)* upgrade rustls and zip past
RUSTSEC-2026-0346..0349/0367..0369
- *(ingest)* the arrow lane builds on its own, and the gate proves it
- *(ingest)* a decode-path package is (name, version), not a name
- *(ingest)* the decoder digest is per lane, carries each crate's
source, and covers the whole decode path
- *(ingest)* NIfTI spatial units to mm, big-endian NIfTI-1, FSL qform
cross-check ([#470](#470))
- *(ingest)* close the five NIfTI decoder bugs and verify the .nii.gz
gzip trailer ([#447](#447))
- *(ingest)* seal the ADR-0058 recipe on the streaming path and surface
it in inspect ([#453](#453))

### Other

- *(ingest)* align the hdf-compound streaming slab to the dataset
chunking ([#464](#464))
</blockquote>

## `tessera-cli`

<blockquote>

##
[0.1.0-alpha.2](tessera-cli-v0.1.0-alpha.1...tessera-cli-v0.1.0-alpha.2)
- 2026-10-19

### Added

- *(cli)* serve array stats from the chunk-index with no decode, and add
stats --json
- *(cli)* add Parquet to the bench compare head-to-head
- *(cli)* bench compare reports BOTH table fixtures
- *(ingest)* generic array ingest — NumPy .npy and .npz (ADR-0056 P1)
- *(cli,core)* tessera provenance walk and inspect --resolve-config
([#490](#490))
- *(ingest)* generic table ingest — tessera ingest table
<PARQUET|IPC|CSV> ([#460](#460))
- *(cli)* tessera bench compare — .tsra vs HDF5 head-to-head, every read
verified, losses included
([#487](#487))
- *(cli)* native dtypes for npy grid output and native max reduction
([#486](#486))
- *(cli)* non-CSV grid output for slice/project, terminal-only preview
caps, read on extra/
([#476](#476))

### Fixed

- *(deps)* upgrade rustls and zip past
RUSTSEC-2026-0346..0349/0367..0369
- *(core,cli)* compute array statistics in exact integer arithmetic
- *(ingest)* the decoder digest is per lane, carries each crate's
source, and covers the whole decode path
- *(cli,io)* pin inspect --json, guard stage-dir downgrade resume, route
verify through the resource cap, 5.5x single-column reads
([#479](#479))
- *(ingest)* seal the ADR-0058 recipe on the streaming path and surface
it in inspect ([#453](#453))
- *(ci)* stop the whitespace autofixers rewriting trycmd snapshots
- *(security)* generate the OpenSSH test key instead of committing one

### Other

- *(deps)* bump dicom-rs to 0.10 to clear the three jxl advisories
- *(book)* put Parquet in the comparison table
- *(book)* report both table fixtures and say which one tracks reality
- *(test)* versioning walkthrough succeeds instead of documenting
blessed errors ([#491](#491))
- ingest cookbook, generated product-schema reference, tested Python
write example, cli.md stubs
([#484](#484))
- *(ingest)* align the hdf-compound streaming slab to the dataset
chunking ([#464](#464))
- clear the lint backlog the new CI gate surfaced
</blockquote>

## `tessera-py`

<blockquote>

##
[0.1.0-alpha.2](tessera-py-v0.1.0-alpha.1...tessera-py-v0.1.0-alpha.2)
- 2026-10-19

### Added

- *(py)* expose with_producer / with_generation on Builder
- *(py)* pin a parent's seal on Builder.add_source
- *(py)* expose the array codec on Builder.add_array

### Other

- *(py)* prove the codec knob round-trips and the default is unchanged
- ingest cookbook, generated product-schema reference, tested Python
write example, cli.md stubs
([#484](#484))
- clear the lint backlog the new CI gate surfaced
</blockquote>

## `tessera-wasm`

<blockquote>

##
[0.1.0-alpha.1](https://github.com/vig-os/tessera/releases/tag/tessera-wasm-v0.1.0-alpha.1)
- 2026-09-23

### Added

- *(signing)* bind signed_at + key_format; ssh-ed25519 loader; usage
tests-as-docs
- *(wasm)* tessera-wasm JS bindings — in-browser verify,
build→bindgen→node gated
([#210](#210))
</blockquote>


</p></details>

---
This PR was generated with
[release-plz](https://github.com/release-plz/release-plz/).
gerchowl added a commit that referenced this pull request Oct 19, 2026
## 🤖 New release

* `tessera-core`: 0.1.0-alpha.2 -> 0.1.0-alpha.3
* `tessera-io`: 0.1.0-alpha.2 -> 0.1.0-alpha.3
* `tessera-ingest`: 0.1.0-alpha.2 -> 0.1.0-alpha.3
* `tessera-cli`: 0.1.0-alpha.2 -> 0.1.0-alpha.3
* `tessera-py`: 0.1.0-alpha.2 -> 0.1.0-alpha.3
* `tessera-wasm`: 0.1.0-alpha.2 -> 0.1.0-alpha.3

<details><summary><i><b>Changelog</b></i></summary><p>

## `tessera-core`

<blockquote>

##
[0.1.0-alpha.2](tessera-core-v0.1.0-alpha.1...tessera-core-v0.1.0-alpha.2)
- 2026-10-19

### Added

- *(cli,core)* tessera provenance walk and inspect --resolve-config
([#490](#490))
- *(ingest)* generic table ingest — tessera ingest table
<PARQUET|IPC|CSV> ([#460](#460))

### Fixed

- *(core,cli)* compute array statistics in exact integer arithmetic

### Other

- ingest cookbook, generated product-schema reference, tested Python
write example, cli.md stubs
([#484](#484))
</blockquote>

## `tessera-io`

<blockquote>

##
[0.1.0-alpha.2](tessera-io-v0.1.0-alpha.1...tessera-io-v0.1.0-alpha.2)
- 2026-10-19

### Added

- *(cli)* serve array stats from the chunk-index with no decode, and add
stats --json
- *(ingest)* generic array ingest — NumPy .npy and .npz (ADR-0056 P1)
- *(cli,core)* tessera provenance walk and inspect --resolve-config
([#490](#490))

### Fixed

- *(io)* chunk statistics on nullable columns with nulls
- *(deps)* upgrade rustls and zip past
RUSTSEC-2026-0346..0349/0367..0369
- *(cli,io)* pin inspect --json, guard stage-dir downgrade resume, route
verify through the resource cap, 5.5x single-column reads
([#479](#479))
- *(io)* gate seal determinism across build profiles and pin the #468
vortex limitation ([#474](#474))
- *(io)* close the encode-trace callsite race under CPU contention
([#465](#465))
- *(ingest)* seal the ADR-0058 recipe on the streaming path and surface
it in inspect ([#453](#453))

### Other

- *(deps)* pin vortex to a patched fork for #468/#472
([#481](#481))
</blockquote>

## `tessera-ingest`

<blockquote>

##
[0.1.0-alpha.2](tessera-ingest-v0.1.0-alpha.1...tessera-ingest-v0.1.0-alpha.2)
- 2026-10-19

### Added

- *(ingest)* generic array ingest — NumPy .npy and .npz (ADR-0056 P1)
- *(ingest)* generic table ingest — tessera ingest table
<PARQUET|IPC|CSV> ([#460](#460))

### Fixed

- *(deps)* upgrade rustls and zip past
RUSTSEC-2026-0346..0349/0367..0369
- *(ingest)* the arrow lane builds on its own, and the gate proves it
- *(ingest)* a decode-path package is (name, version), not a name
- *(ingest)* the decoder digest is per lane, carries each crate's
source, and covers the whole decode path
- *(ingest)* NIfTI spatial units to mm, big-endian NIfTI-1, FSL qform
cross-check ([#470](#470))
- *(ingest)* close the five NIfTI decoder bugs and verify the .nii.gz
gzip trailer ([#447](#447))
- *(ingest)* seal the ADR-0058 recipe on the streaming path and surface
it in inspect ([#453](#453))

### Other

- *(ingest)* align the hdf-compound streaming slab to the dataset
chunking ([#464](#464))
</blockquote>

## `tessera-cli`

<blockquote>

##
[0.1.0-alpha.3](tessera-cli-v0.1.0-alpha.2...tessera-cli-v0.1.0-alpha.3)
- 2026-10-19

### Fixed

- *(cli)* make bench compare --cold Linux-only and refuse it elsewhere
</blockquote>

## `tessera-py`

<blockquote>

##
[0.1.0-alpha.3](tessera-py-v0.1.0-alpha.2...tessera-py-v0.1.0-alpha.3)
- 2026-10-19

### Fixed

- *(py)* link the _native extension dylib on macOS
</blockquote>

## `tessera-wasm`

<blockquote>

##
[0.1.0-alpha.1](https://github.com/vig-os/tessera/releases/tag/tessera-wasm-v0.1.0-alpha.1)
- 2026-09-23

### Added

- *(signing)* bind signed_at + key_format; ssh-ed25519 loader; usage
tests-as-docs
- *(wasm)* tessera-wasm JS bindings — in-browser verify,
build→bindgen→node gated
([#210](#210))
</blockquote>


</p></details>

---
This PR was generated with
[release-plz](https://github.com/release-plz/release-plz/).
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