Files
clawhdf5/CHANGELOG.md
Omar Sobh 429c29b76b
CI / test (push) Failing after 3s
docs: sync README/ROADMAP/CLAUDE/CHANGELOG with Tier 1-4 hardening work
README.md:
- Fix badly stale LongMemEval numbers (badge said Hit@5 46%, table showed
  fabricated ~46%/~0.34/~72% figures that never matched BENCHMARKS.md's
  actual results of Hit@5 100% session / 84.4% turn-level, MRR 1.0/0.6597)
- Remove clawhdf5-types from the Crate Map — that crate was removed in an
  earlier cleanup pass but the README diagram was never updated; fix the
  crate count (16, not 17) and stale line-of-code figures (72,087/84K -> ~92K)
- Fix a dead #benchmarks badge anchor (no such heading exists) -> #performance
- Document the new clawhdf5-ann `parallel` feature (had no Feature Flags entry)
- Note WAL's CRC32 per-entry check, link the new tank LongMemEval/SIMD/
  vector-search reproduction section, update stale test-count comment
  (417+ -> 1,650+) and Phase 2 roadmap blurb (LongMemEval is now done)

ROADMAP.md:
- Check off "Academic benchmark cross-validation" (done via the tank
  LongMemEval re-run) and add a new "Recently closed out" section
  summarizing the Tier 3-4 hardening pass (Android JNI validation, pyo3
  bump, WAL CRC32, bounds-check audit + fuzz harness that found 3 real
  bugs, HNSW optional parallel feature, workspace.dependencies)
- Update stale test count (1,546 -> 1,650+) and last-updated date

CLAUDE.md: mention WAL's per-entry CRC32 check

CHANGELOG.md: add Security/Performance/Architecture/Documentation entries
under Unreleased summarizing all of Tiers 1-4 (this had not been touched
since 2026-06-04, predating the entire hardening pass)
2026-08-05 15:26:47 -07:00

19 KiB

Changelog

Unreleased

Security

  • clawhdf5-format: bounded decompression output (MAX_DECOMPRESS_SIZE) for deflate/lz4/zstd/pcodec so a crafted compressed chunk can't drive an unbounded allocation (memory-exhaustion DoS).
  • clawhdf5-format: chunked_read.rs/data_read.rs/local_heap.rs bounds audit — added ensure_len overflow guards at every plain-arithmetic offset+size check, a recursion-depth guard against a crafted self-referencing/cyclic B-tree chunk index, a fix for an unguarded compound-datatype byte_offset overrun in read_compound_fields, and an ndims - 1 underflow guard for degenerate zero-dimension chunked layouts. Added a new fuzz_dataset_read cargo-fuzz target (walks every dataset in a parsed file and exercises the contiguous/chunked/compact raw-data read paths) which found and fixed 3 real crash bugs — an integer-multiply overflow in copy_chunk_to_output's N-D assembly path, the ndims - 1 underflow above, and an overflow in local_heap.rs — within the first few fuzzing runs.
  • clawhdf5-format: btree_v1.rs overflow-safe bounds checks via a local ensure_len helper, closing a usize-overflow panic reachable from a crafted near-usize::MAX B-tree offset.
  • clawhdf5-agent: WAL length-prefix caps (MAX_WAL_FIELD_LEN, 64 MiB) reject a corrupted/truncated length claim before allocating. Followed by a full per-entry CRC32 trailer (WAL_VERSION bumped to 2) — a bit-flip inside an entry now stops replay cleanly instead of silently accepting corrupted data. Old-format WAL files are still read correctly and migrated to the new format on next open.
  • clawhdf5-android: validate embedding_len/query_embedding_len against the handle's configured embedding_dim (and reject null pointers) before constructing a slice from a raw pointer in edgehdf5_save / edgehdf5_hybrid_search.
  • clawhdf5-py: bump pyo3/numpy 0.280.29, clearing two RUSTSEC advisories (OOB read in PyList/PyTuple iterator; missing Sync bound on PyCFunction::new_closure).
  • Clarified that the integrity hashes in clawhdf5-agent::provenance (FNV-1a) and clawhdf5-format::provenance (SHA-256) are unkeyed and detect only accidental corruption, not tampering — doc-only change, no behavior change.

Performance

  • clawhdf5-format: chunk cache lookup is now O(1) (slot_index: HashMap) instead of a linear scan, and cache hits return a shared Arc instead of cloning the decompressed buffer — the hottest path in chunked reads.
  • clawhdf5-ann: optional parallel feature (rayon) parallelizes HNSW's prune_connections neighbor-distance computation. The outer build/insert loop is deliberately left sequential — it has genuine cross-iteration data dependencies and needs its own correctness-focused design pass.
  • clawhdf5-format/chunked_read.rs: removed 12 unnecessary chunk_dimensions[..rank].to_vec() allocations where callees already accept &[u32].

Architecture

  • Added .gitea/workflows/ci.yml, actually wiring the long-existing scripts/ci-test.sh (fmt, clippy, tests, no_std check) into CI on every push/PR to main. Fixed stale package names in ci-test.sh/ check-nostd.sh that had been silently no-op'ing the clawhdf5-py exclusion and the no_std check.
  • Fixed a genuine no_std build break in clawhdf5-format (uncovered once the no_std CI check actually started running): core::sync::atomic::AtomicU64 doesn't exist on thumbv7em-none-eabihf (switched to portable-atomic), missing alloc imports for Box/Vec/format! on a few no_std paths, and f64::powi (std/libm-only) replaced with a local exponentiation-by-squaring helper in the scale-offset filter.
  • Added [workspace.dependencies] for tempfile/criterion/half/serde, fixing a real version skew on half (2 vs 2.7 across crates).
  • Fixed version skew: clawhdf5-py (pyproject.toml) and packages/clawhdf5-node (package.json) were both behind the actual crate version (2.1.0).
  • Documented that the mpi-io feature's read/write paths are root-read +broadcast / gather-to-rank-0, not true collective I/O.

Documentation

  • BENCHMARKS.md: re-ran the previously-undated "LongMemEval Results", "SIMD & Parallelism", and "Vector Search Latency"/"Comparison to MemX" sections on a second machine (tank, Ryzen 7 7800X3D) with explicit dates and reproduce commands. Found and corrected a methodology issue in the SIMD/Parallelism benchmark selection (several originally-compared benchmarks didn't actually isolate the scalar/SIMD/parallel axis).
  • README.md / ROADMAP.md / CLAUDE.md: corrected several stale facts — the clawhdf5-types crate (removed earlier) was still listed in the README crate map; the LongMemEval numbers in the README badge and table didn't match the actual (much better) benchmark results in BENCHMARKS.md; total line-of-code and test-count figures were stale; clawhdf5-gpu's CubeCL→wgpu correction; documented the new clawhdf5-ann parallel feature flag, which had no entry in the Feature Flags table.

New Features

  • clawhdf5-migrate: substantial engine improvements:
    • Real content validation — the post-migration check now reads the written HDF5 back and compares actual content (chunk text, embeddings, and every session/entity/relation field) against the source, not just row counts. A representative sample of chunk rows is verified by default; --validate-full checks every row. A corrupt migration that preserves counts no longer passes.
    • Configurable schema — table names are no longer hardcoded; queries are built from a SchemaConfig (table + ordered column names, defaulting to the ZeroClaw layout) with --chunks-table / --sessions-table / --entities-table / --relations-table overrides.
    • Streaming count pass--dry-run now does a COUNT(*)-only pass per table instead of loading every row into memory.
    • Incremental migration--incremental reads the existing output, reads only source chunks newer than the last migrated id, and appends them (refreshing the metadata groups), instead of re-migrating everything.
  • clawhdf5-format: read IEEE-754 half-precision (f16) floats. read_as_f32 / read_as_f64 previously only handled 4- and 8-byte floats; 2-byte floats (e.g. float16-stored embeddings) now decode via a no_std-safe bit conversion.
  • clawhdf5-format: write multi-block fractal heaps (root indirect block). Dense attribute and dense link storage previously capped at a single direct block (~64 KiB of heap data — a few thousand attributes/links). When the objects exceed one direct block, the heap now lays out a root indirect block (FHIB) over multiple direct blocks sized by the doubling table, distributing objects across blocks with correct per-block heap offsets. Validated end-to-end: a 2,500-attribute object and a 2,500-link group round-trip through our reader and are read correctly by h5py. (Objects still may not span a block — no huge-object path.)
  • clawhdf5-format: write dense group link storage (fractal heap + v2 B-tree). A group with more than 8 links (libhdf5's compact max_compact default) is now written densely — its links live in a fractal heap indexed by a v2 B-tree of type 5 (link-name index) referenced from the group's LinkInfo message — instead of as inline Link messages. This matches libhdf5's compact→dense switchover and keeps large groups out of the object header. Reverse-engineered against libhdf5: link heaps use heap_id_length 7 / max_heap_size 32 (vs 8 / 40 for attributes). The shared single-direct-block fractal-heap builder is now parameterized and used by both dense attributes and dense links. Validated end-to-end: our reader round-trips, and h5py reads the dense groups we write. (Single direct block — up to ~a couple thousand links per group; beyond that needs indirect blocks, still unsupported.)

Robustness

  • clawhdf5-format: harden the readers added this cycle against malformed / hostile input — they parse untrusted bytes and must return errors, never panic, OOM, or recurse without bound. Fixed concrete vectors found by audit and locked in with adversarial tests:
    • Paged Fixed Array: 1 << max_nelmts_bits shift overflow (a u8 ≥ 64); element/page offset multiplications now checked; element count bounded by file size.
    • H5S selection decoder: ALL/NONE no longer claim 16 bytes they don't have; hyperslab rank capped at 32 (H5S_MAX_RANK) to stop a giant allocation; iter_linear coordinate/stride/product arithmetic is checked.
    • VDS mapping parser: no pre-allocation from the untrusted nused; all selection slicing is bounds-checked.
    • scale-offset / N-Bit filters: 1 << minbits overflow at minbits == 64; N-Bit bit_offset + precision overflow; N-Bit type-tree recursion depth capped (no stack overflow from a crafted nested tree); element counts bounded by the chunk's expected decompressed size so a bogus count can't drive a huge allocation.
    • Virtual Dataset assembly: a virtual dataset whose source is itself virtual (a cycle) now errors instead of recursing into a stack overflow.

New Features

  • clawhdf5-agent: compress fixed-length string datasets (memory text chunks, session summaries, ids, tags, entity/relation names, …). These were always stored uncompressed with a "chunked compound not yet supported" note that was simply stale — chunked writes work for fixed-size string/compound datatypes like any other. write_string_dataset now chunks + deflates a string dataset once its payload reaches 4 KiB, so large, highly-redundant NullPad content shrinks substantially while tiny metadata stays contiguous (no chunk-overhead bloat).
  • clawhdf5-format: decode the scale-offset filter (id 6) — both the integer variant (H5Z_SO_INT) and the floating-point D-scale variant (H5Z_SO_FLOAT_DSCALE). Handles signed/unsigned int sizes, f32/f64, negative minima, decimal scale factors and fill values; reverse-engineered against HDF5 2.0 and validated end-to-end. The float E-scale variant remains unsupported.
  • clawhdf5-format: decode the N-Bit filter (id 5) — atomic, compound and array layouts (the full recursive type tree, nestable to any depth), previously unsupported. Signed and unsigned reduced-precision integers and float members all read end-to-end, validated against HDF5 2.0.

New Features

  • clawhdf5 / clawhdf5-format: read external-file Virtual Datasets (VDS). The format layer gains read_raw_data_full_with_resolver and a VdsSourceResolver callback (Fn(&str) -> Option<Vec<u8>>) that maps a stored source file name to its bytes, so the pure-byte reader can pull in external sources without a filesystem of its own. The clawhdf5 File API wires a default resolver that reads sibling source files relative to the opened file's directory, so File::open(...).dataset(...).read_*() now transparently assembles cross-file VDS. A source file the resolver cannot supply leaves its region at the fill value (matching HDF5); an external source with no resolver at all is a clean error. In-memory files (File::from_bytes) have no directory, so only same-file VDS resolves there.
  • clawhdf5-format: assemble same-file Virtual Datasets (VDS) of any rank. Previously a virtual layout returned UnsupportedVersion. The reader now decodes the global-heap mapping block (reverse-engineered against HDF5 2.0: version · nused · [source-file · source-dataset · source-selection · virtual-selection]* · checksum, including the block-version-1 same-file marker), decodes the H5S source/virtual dataspace selections (ALL, NONE, and version-3 regular hyperslabs), reads each same-file source dataset, and scatters its selected elements into the virtual buffer in row-major order (so multi-dimensional block mappings land correctly); unmapped regions are left at the zero fill value. External-file sources return a clean unsupported error. The previous parse_vds_mappings used a guessed layout that did not match real files and is replaced.

Tests

  • clawhdf5-format: regression test for scale-offset float E-scale datasets. The HDF5 library does not implement E-scale encoding — when asked for it (cd_values[0] = 1) it stores the chunk raw and sets the chunk filter mask to skip the filter — so these files read back verbatim purely by honoring the per-chunk filter mask. The test locks in that behavior against a fixture produced via the HDF5 low-level API; no E-scale decoder is needed.

Bug Fixes

  • clawhdf5-format: read multi-direct-block fractal heaps. The reader split direct vs indirect block rows using the FRHP "Starting # of Rows in Root Indirect Block" field (a constant, typically 1), so any heap whose data spans more than one direct block — common in libhdf5 files with a large group or many dense attributes — was misread as having indirect blocks and failed with InvalidFractalHeapSignature. The split is now derived from the heap geometry (max_direct_rows = log2(max_direct / start) + 2). Validated against an h5py-written 400-dense-attribute group (root indirect block, 4 rows, 13 direct blocks).
  • clawhdf5-format: scope the per-file chunk cache by dataset. The shared ChunkCache built its chunk index once and reused it for every chunked dataset in the file, keyed only by chunk coordinate with no dataset discrimination. With a single chunked dataset per file this was latent; once a file holds two chunked datasets of different rank (e.g. a 1-D compressed string array and the 2-D embeddings matrix), the first dataset's index was reused for the second, panicking with an out-of-bounds chunk coordinate. The cache now rebinds (dropping its index, chunk-index map, layout, and decompressed slots) whenever the dataset being read changes, while still caching repeated/sequential access to the same dataset.
  • clawhdf5-format: read paged Fixed Array chunk indexes. A filtered, fixed-dimension dataset with more than one data-block page (>1024 chunks by default) previously failed with "paged Fixed Array data blocks not yet supported". The reader now walks the page-init bitmap (MSB-first), skips uninitialized pages, and resolves each page's fixed full-size slot (including the short final page). Reverse-engineered and validated end-to-end against an HDF5 2.0 file.
  • clawhdf5-format: read array-typed datatypes (e.g. an array-typed compound member) via read_as_i32/i64/u64/f32/f64 — previously a TypeMismatch. The array is read as a flat sequence of its base elements (recursing for nested arrays), applying base-type precision rules.
  • clawhdf5-format: sign-extend reduced-precision fixed-point integers on read. A signed integer whose datatype precision is smaller than its storage size is stored zero-filled, so e.g. a 16-bit-precision -1 previously read as 65535. The integer read paths now extract the precision field and sign-extend (full-width types are unchanged). Completes signed N-Bit reads and also fixes un-filtered reduced-precision integer datasets.
  • clawhdf5-format: read datasets written by modern HDF5 (1.14+/2.0, i.e. libver=latest). Compound (class 6) and array (class 10) datatype version 5 messages and data layout version 5 messages were rejected as invalid; they reuse the v3/v4 binary structure, so they are now accepted. This unblocks reading compound types and — critically — every chunked/compressed dataset written by HDF5 2.0. Found by running the h5py interop tests against h5py 3.16 / HDF5 2.0.

Performance

  • clawhdf5-format: chunked writes now compress all chunks up front via compress_all_chunks, running across rayon threads under the parallel feature when there are more than 4 filtered chunks. On-disk layout is unchanged. Speeds up compressed embedding writes in clawhdf5-agent (which enables parallel).

Documentation

  • Fix stale package names across all 13 per-crate READMEs (rustyhdf5-* / edgehdf5-*clawhdf5-*, usage versions → 2.1.0).
  • Correct README workspace/test/crate stats and the CLAUDE.md CLI subcommand list; document the hnsw and format compression/checksum feature flags and the entity_extract / async_memory modules.

v2.1.0 (2026-06-03)

New Features

  • clawhdf5-agent: HNSW now backs the vector stage of hybrid_search. The hnsw feature is on by default, so semantic search uses the approximate clawhdf5-ann index instead of a linear cosine scan. The index mirrors the memory cache (node id == cache index) and self-heals — it rebuilds whenever it drifts from the cache length, so no mutation path can desync it. Non-indexable stores (no/zero-dim/mixed embeddings) and dimension-mismatched queries fall back to the exact linear scan. Disable with --no-default-features --features float16 for exact search.
  • clawhdf5-ann: HNSW is now a live, mutable index — added insert, mark_deleted (soft-delete bitset; deleted nodes are traversed for connectivity but never returned), compact (drops deleted vectors and renumbers survivors), and new (empty index). Serialization gains a format version tag (HNSW_FORMAT_VERSION = 2) and persists the deleted bitset; pre-existing v1 files still load.
  • clawhdf5-agent: hybrid::merge_vector_keyword exposes the shared normalize-and-fuse step used by both the linear and HNSW vector paths.
  • Expose max_dimensions() API on Dataset, MmapDataset, and LazyDataset
  • NetCDF-4 unlimited dimension detection now works correctly
  • Python bindings (clawhdf5-py) build and link on macOS with system Python

Bug Fixes

  • clawhdf5-py: upgrade PyO3 and numpy 0.230.28 so the bindings build on Python 3.14 (PyO3 0.23 capped at 3.13 and hard-failed cargo build --workspace). Updated for the removed PyObject alias (Py<PyAny>) and the Python::allow_threadsPython::detach rename.
  • Fix GPU L2 distance test (squared vs actual L2 mismatch in test helper)
  • Mark Android JNI functions as unsafe for Rust 2024 edition compliance
  • Add # Safety documentation to all public unsafe extern functions
  • Fix all clippy warnings: needless_range_loop, manual_strip, ptr_arg, etc.
  • Rename RelationType::from_str to from_label to avoid trait confusion
  • Isolate h5py interop tests with #[ignore] when h5py unavailable

Code Quality

  • Full rustfmt pass across workspace (61 files)
  • Refine inner unsafe blocks for Rust 2024 edition style
  • Zero clippy warnings, zero clippy errors across entire workspace
  • 1,546 tests passing, 0 failures

v2.0.0 (2026-03-19)

  • Unified rustyhdf5 (11 crates) and edgehdf5 (4 crates) into a single workspace
  • All crates renamed to clawhdf5-* prefix
  • Version bumped to 2.0.0 across all crates
  • Git dependencies replaced with in-workspace path dependencies
  • Added agent feature flag to clawhdf5-agent