5 Commits
Author SHA1 Message Date
osobhandClaude Fable 5.1 a8ab9ca054 Merge release/v2.2.0: native complex datatype fix, v2.2.0 release, repository URL
CI / test (push) Failing after 11s
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-09-18 20:58:34 -07:00
osobhandClaude Fable 5.1 2053b69f07 chore(release): v2.2.0, point repository URLs at git.redclaw.dev
Bump all workspace crates, the node package and pyproject to 2.2.0 and
finalize the changelog. The repository URL in every manifest pointed at a
GitHub location that does not resolve; use the real origin.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-09-18 20:58:29 -07:00
osobhandClaude Fable 5.1 b55b7dbac5 fix(format): parse HDF5 2.0 native complex datatypes (class 11)
Class 11 (datatype version 5) properties are a single base floating-point
datatype message, not a compound-style member list. The old parser read the
base type's bytes as member names, yielding a garbage datatype, and failed
with UnexpectedEof when a complex type was nested in a compound.

Parse the base type and surface the type as the equivalent {r, i} compound
(the shape h5py writes for numpy complex dtypes), with a size check against
the base type. Covered by byte-level tests taken from HDF5 2.0 output and an
h5py end-to-end test (writer_h5py_tests is now 27/27 against HDF5 2.0.0).

Found while validating a user report of InvalidDatatypeVersion
{ class: 6, version: 5 } against v2.1.0 (already fixed on main in a13ff51,
never released). Add docs/known-issues.md recording that report, this bug,
the open reference-v4 gap and a gpu_tests parallel-run hang; credit the
reporter in the changelog.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-09-18 20:58:29 -07:00
osobh 48c745a960 Merge PR #2: performance, security and provenance hardening + two audit fixes
CI / test (push) Canceled after 0s
ann/io/migrate/agent work from three agent missions, an independent audit, and the two defects it found: the cosine near-zero guard weakened during the SIMD migration, and WAL appends after a torn tail being silently unreplayable. 52 suites green; both fixes proven by negative control.
2026-08-17 14:22:13 +00:00
clawhdf5 committer agentandClaude Sonnet 5 377c8b6f17 fix(accel): restore f32::EPSILON near-zero-denom guard in cosine_similarity
CI / test (pull_request) Canceled after 0s
The SIMD migration weakened the near-zero-norm guard in all four
clawhdf5-accel cosine_similarity backends (scalar/avx2/avx512/neon)
from `denom < f32::EPSILON` to `denom == 0.0`. Vectors with a tiny
but nonzero norm (denom in (0, 1.19e-7)) fell through to dot/denom
and scored as identical instead of maximally dissimilar, diverging
from the pre-SIMD scalar loop's documented fallback behavior.

Restores the epsilon threshold in all four backends so
`1.0 - cosine_similarity(...)` in hnsw.rs::compute_distance
reproduces the old fallback exactly. Adds regression tests in
clawhdf5-accel and clawhdf5-ann locking in the near-zero-norm case.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-17 13:28:43 +00:00
31 changed files with 551 additions and 85 deletions
+11 -1
View File
@@ -1,6 +1,6 @@
# Changelog # Changelog
## Unreleased ## v2.2.0 (2026-09-18)
### Security ### Security
- `clawhdf5-format`: bounded decompression output (`MAX_DECOMPRESS_SIZE`) for - `clawhdf5-format`: bounded decompression output (`MAX_DECOMPRESS_SIZE`) for
@@ -245,6 +245,16 @@
reading compound types and — critically — every chunked/compressed dataset reading compound types and — critically — every chunked/compressed dataset
written by HDF5 2.0. Found by running the h5py interop tests against written by HDF5 2.0. Found by running the h5py interop tests against
h5py 3.16 / HDF5 2.0. h5py 3.16 / HDF5 2.0.
Independently reported (with a patch) against the v2.1.0 tag by
M. Scot Breitenfeld (The HDF Group) — v2.1.0 predates this fix.
- `clawhdf5-format`: parse HDF5 2.0 native complex datatypes (class 11,
datatype version 5, e.g. `H5T_COMPLEX_IEEE_F64LE`). The properties are a
single base floating-point datatype, not a compound-style member list; the
old parser read the base type's bytes as member names, producing a garbage
datatype, and failed with `UnexpectedEof` when a complex type was nested in
a compound. It is now surfaced as the equivalent `{r, i}` compound (the
shape h5py writes for numpy complex dtypes), with a size check against the
base type. Validated end-to-end against an HDF5 2.0-written file.
### Performance ### Performance
- `clawhdf5-format`: chunked writes now compress all chunks up front via - `clawhdf5-format`: chunked writes now compress all chunks up front via
+2 -2
View File
@@ -21,10 +21,10 @@ members = [
resolver = "2" resolver = "2"
[workspace.package] [workspace.package]
version = "2.1.0" version = "2.2.0"
edition = "2024" edition = "2024"
license = "MIT" license = "MIT"
repository = "https://github.com/redclawsystems/clawhdf5" repository = "https://git.redclaw.dev/quantumclaw/clawhdf5"
[workspace.dependencies] [workspace.dependencies]
tempfile = "3" tempfile = "3"
+2 -2
View File
@@ -1,10 +1,10 @@
[package] [package]
name = "clawhdf5-accel" name = "clawhdf5-accel"
version = "2.1.0" version = "2.2.0"
edition = "2024" edition = "2024"
description = "SIMD-accelerated operations for rustyhdf5" description = "SIMD-accelerated operations for rustyhdf5"
license = "MIT" license = "MIT"
repository = "https://github.com/redclawsystems/clawhdf5" repository = "https://git.redclaw.dev/quantumclaw/clawhdf5"
readme = "README.md" readme = "README.md"
keywords = ["hdf5", "simd", "acceleration", "performance"] keywords = ["hdf5", "simd", "acceleration", "performance"]
categories = ["science", "algorithms"] categories = ["science", "algorithms"]
+1 -1
View File
@@ -111,7 +111,7 @@ pub unsafe fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
} }
let denom = (norm_a * norm_b).sqrt(); let denom = (norm_a * norm_b).sqrt();
if denom == 0.0 { 0.0 } else { dot / denom } if denom < f32::EPSILON { 0.0 } else { dot / denom }
} }
} }
+1 -1
View File
@@ -89,7 +89,7 @@ pub unsafe fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
} }
let denom = (norm_a * norm_b).sqrt(); let denom = (norm_a * norm_b).sqrt();
if denom == 0.0 { 0.0 } else { dot / denom } if denom < f32::EPSILON { 0.0 } else { dot / denom }
} }
} }
+12
View File
@@ -361,6 +361,18 @@ mod tests {
assert!(approx_eq(cosine_similarity(&a, &b), 0.0, EPSILON)); assert!(approx_eq(cosine_similarity(&a, &b), 0.0, EPSILON));
} }
#[test]
fn test_cosine_near_zero_norm_clamped() {
// denom = 1e-4 * 1e-4 = 1e-8, comfortably below f32::EPSILON
// (~1.19e-7) but not exactly 0.0 — must still clamp to 0.0 so
// callers computing `1.0 - cosine_similarity(...)` treat these
// as maximally dissimilar, matching the pre-SIMD scalar guard.
let a = [1e-4f32];
let b = [1e-4f32];
assert_eq!(cosine_similarity(&a, &b), 0.0);
assert_eq!(scalar::cosine_similarity(&a, &b), 0.0);
}
#[test] #[test]
fn test_cosine_scalar_vs_dispatch() { fn test_cosine_scalar_vs_dispatch() {
let a: Vec<f32> = (0..384).map(|i| (i as f32).sin()).collect(); let a: Vec<f32> = (0..384).map(|i| (i as f32).sin()).collect();
+1 -1
View File
@@ -94,7 +94,7 @@ pub unsafe fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
} }
let denom = (norm_a * norm_b).sqrt(); let denom = (norm_a * norm_b).sqrt();
if denom == 0.0 { 0.0 } else { dot / denom } if denom < f32::EPSILON { 0.0 } else { dot / denom }
} }
/// NEON L2 distance. /// NEON L2 distance.
+1 -1
View File
@@ -21,7 +21,7 @@ pub fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
norm_b += y * y; norm_b += y * y;
} }
let denom = (norm_a * norm_b).sqrt(); let denom = (norm_a * norm_b).sqrt();
if denom == 0.0 { 0.0 } else { dot / denom } if denom < f32::EPSILON { 0.0 } else { dot / denom }
} }
pub fn batch_cosine(query: &[f32], vectors: &[&[f32]], results: &mut [(usize, f32)]) { pub fn batch_cosine(query: &[f32], vectors: &[&[f32]], results: &mut [(usize, f32)]) {
+8 -8
View File
@@ -1,21 +1,21 @@
[package] [package]
name = "clawhdf5-agent" name = "clawhdf5-agent"
version = "2.1.0" version = "2.2.0"
edition = "2024" edition = "2024"
description = "HDF5-backed persistent memory store for on-device AI agents" description = "HDF5-backed persistent memory store for on-device AI agents"
license = "MIT" license = "MIT"
repository = "https://github.com/redclawsystems/clawhdf5" repository = "https://git.redclaw.dev/quantumclaw/clawhdf5"
readme = "README.md" readme = "README.md"
keywords = ["agent", "memory", "hdf5", "vector-search", "embedding"] keywords = ["agent", "memory", "hdf5", "vector-search", "embedding"]
categories = ["database", "science", "algorithms"] categories = ["database", "science", "algorithms"]
[dependencies] [dependencies]
clawhdf5-format = { path = "../clawhdf5-format", version = "2.1.0", features = ["parallel", "fast-checksum"] } clawhdf5-format = { path = "../clawhdf5-format", version = "2.2.0", features = ["parallel", "fast-checksum"] }
clawhdf5 = { path = "../clawhdf5", version = "2.1.0" } clawhdf5 = { path = "../clawhdf5", version = "2.2.0" }
clawhdf5-io = { path = "../clawhdf5-io", version = "2.1.0", features = ["mmap"] } clawhdf5-io = { path = "../clawhdf5-io", version = "2.2.0", features = ["mmap"] }
clawhdf5-accel = { path = "../clawhdf5-accel", version = "2.1.0" } clawhdf5-accel = { path = "../clawhdf5-accel", version = "2.2.0" }
clawhdf5-ann = { path = "../clawhdf5-ann", version = "2.1.0", optional = true } clawhdf5-ann = { path = "../clawhdf5-ann", version = "2.2.0", optional = true }
clawhdf5-gpu = { path = "../clawhdf5-gpu", version = "2.1.0", optional = true, default-features = false } clawhdf5-gpu = { path = "../clawhdf5-gpu", version = "2.2.0", optional = true, default-features = false }
serde = { workspace = true } serde = { workspace = true }
byteorder = "1" byteorder = "1"
half = { workspace = true, optional = true } half = { workspace = true, optional = true }
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "clawhdf5-android" name = "clawhdf5-android"
version = "2.1.0" version = "2.2.0"
edition = "2024" edition = "2024"
description = "Android JNI bridge for edgehdf5-memory HDF5 backend" description = "Android JNI bridge for edgehdf5-memory HDF5 backend"
license = "MIT" license = "MIT"
+5 -5
View File
@@ -1,18 +1,18 @@
[package] [package]
name = "clawhdf5-ann" name = "clawhdf5-ann"
version = "2.1.0" version = "2.2.0"
edition = "2024" edition = "2024"
description = "HNSW approximate nearest neighbor index stored as HDF5" description = "HNSW approximate nearest neighbor index stored as HDF5"
license = "MIT" license = "MIT"
repository = "https://github.com/redclawsystems/clawhdf5" repository = "https://git.redclaw.dev/quantumclaw/clawhdf5"
readme = "README.md" readme = "README.md"
keywords = ["hdf5", "ann", "hnsw", "nearest-neighbor"] keywords = ["hdf5", "ann", "hnsw", "nearest-neighbor"]
categories = ["algorithms", "science"] categories = ["algorithms", "science"]
[dependencies] [dependencies]
clawhdf5-format = { path = "../clawhdf5-format", version = "2.1.0" } clawhdf5-format = { path = "../clawhdf5-format", version = "2.2.0" }
clawhdf5-io = { path = "../clawhdf5-io", version = "2.1.0" } clawhdf5-io = { path = "../clawhdf5-io", version = "2.2.0" }
clawhdf5-accel = { path = "../clawhdf5-accel", version = "2.1.0" } clawhdf5-accel = { path = "../clawhdf5-accel", version = "2.2.0" }
rayon = { version = "1", optional = true } rayon = { version = "1", optional = true }
[features] [features]
+12
View File
@@ -1300,6 +1300,18 @@ mod tests {
assert!((d - 1.0).abs() < 1e-6); // zero vector -> distance 1 assert!((d - 1.0).abs() < 1e-6); // zero vector -> distance 1
} }
#[test]
fn cosine_near_zero_vector() {
// Tiny-but-nonzero, identical-direction vectors: denom is well
// below f32::EPSILON but not exactly 0.0. Must still be treated
// as a degenerate/unreliable direction (distance 1, "maximally
// dissimilar"), not as an exact match (distance 0).
let a = vec![1e-4, 1e-4];
let b = vec![1e-4, 1e-4];
let d = compute_distance(&a, &b, DistanceMetric::Cosine);
assert!((d - 1.0).abs() < 1e-6);
}
#[test] #[test]
fn insert_into_empty_index() { fn insert_into_empty_index() {
let mut index = HnswIndex::new(4, 16, DistanceMetric::L2); let mut index = HnswIndex::new(4, 16, DistanceMetric::L2);
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "clawhdf5-bench" name = "clawhdf5-bench"
version = "2.1.0" version = "2.2.0"
edition = "2024" edition = "2024"
description = "Benchmark harnesses for clawhdf5-agent (Track 8)" description = "Benchmark harnesses for clawhdf5-agent (Track 8)"
license = "MIT" license = "MIT"
+3 -3
View File
@@ -1,10 +1,10 @@
[package] [package]
name = "clawhdf5-cli" name = "clawhdf5-cli"
version = "2.1.0" version = "2.2.0"
edition = "2024" edition = "2024"
license = "MIT" license = "MIT"
description = "CLI for clawhdf5 agent memory — create, save, search, recall, stats" description = "CLI for clawhdf5 agent memory — create, save, search, recall, stats"
repository = "https://github.com/redclawsystems/clawhdf5" repository = "https://git.redclaw.dev/quantumclaw/clawhdf5"
keywords = ["hdf5", "ai", "memory", "agent", "cli"] keywords = ["hdf5", "ai", "memory", "agent", "cli"]
categories = ["command-line-utilities", "science"] categories = ["command-line-utilities", "science"]
readme = "../../README.md" readme = "../../README.md"
@@ -14,7 +14,7 @@ name = "clawhdf5"
path = "src/main.rs" path = "src/main.rs"
[dependencies] [dependencies]
clawhdf5-agent = { path = "../clawhdf5-agent", version = "2.1.0" } clawhdf5-agent = { path = "../clawhdf5-agent", version = "2.2.0" }
clap = { version = "4", features = ["derive", "env"] } clap = { version = "4", features = ["derive", "env"] }
serde_json = "1" serde_json = "1"
serde = { workspace = true } serde = { workspace = true }
+2 -2
View File
@@ -1,10 +1,10 @@
[package] [package]
name = "clawhdf5-derive" name = "clawhdf5-derive"
version = "2.1.0" version = "2.2.0"
edition = "2024" edition = "2024"
description = "Derive macros for rustyhdf5 HDF5 traits" description = "Derive macros for rustyhdf5 HDF5 traits"
license = "MIT" license = "MIT"
repository = "https://github.com/redclawsystems/clawhdf5" repository = "https://git.redclaw.dev/quantumclaw/clawhdf5"
readme = "README.md" readme = "README.md"
keywords = ["hdf5", "derive", "macros", "science"] keywords = ["hdf5", "derive", "macros", "science"]
categories = ["development-tools::procedural-macro-helpers"] categories = ["development-tools::procedural-macro-helpers"]
+2 -2
View File
@@ -1,10 +1,10 @@
[package] [package]
name = "clawhdf5-filters" name = "clawhdf5-filters"
version = "2.1.0" version = "2.2.0"
edition = "2024" edition = "2024"
description = "Filter and compression pipeline for clawhdf5" description = "Filter and compression pipeline for clawhdf5"
license = "MIT" license = "MIT"
repository = "https://github.com/redclawsystems/clawhdf5" repository = "https://git.redclaw.dev/quantumclaw/clawhdf5"
readme = "README.md" readme = "README.md"
keywords = ["hdf5", "compression", "deflate", "filters"] keywords = ["hdf5", "compression", "deflate", "filters"]
categories = ["compression", "science"] categories = ["compression", "science"]
+3 -3
View File
@@ -1,10 +1,10 @@
[package] [package]
name = "clawhdf5-format" name = "clawhdf5-format"
version = "2.1.0" version = "2.2.0"
edition = "2024" edition = "2024"
description = "Pure-Rust HDF5 binary format parsing and writing — no C dependencies" description = "Pure-Rust HDF5 binary format parsing and writing — no C dependencies"
license = "MIT" license = "MIT"
repository = "https://github.com/redclawsystems/clawhdf5" repository = "https://git.redclaw.dev/quantumclaw/clawhdf5"
readme = "README.md" readme = "README.md"
keywords = ["hdf5", "science", "data", "binary", "no-std"] keywords = ["hdf5", "science", "data", "binary", "no-std"]
categories = ["parser-implementations", "science", "encoding", "no-std"] categories = ["parser-implementations", "science", "encoding", "no-std"]
@@ -25,7 +25,7 @@ pco = { version = "1.0", optional = true }
[dev-dependencies] [dev-dependencies]
serde_json = "1" serde_json = "1"
criterion = { workspace = true } criterion = { workspace = true }
clawhdf5-derive = { path = "../clawhdf5-derive", version = "2.1.0" } clawhdf5-derive = { path = "../clawhdf5-derive", version = "2.2.0" }
[[bench]] [[bench]]
name = "bench" name = "bench"
+100 -19
View File
@@ -546,27 +546,39 @@ impl Datatype {
} }
} }
11 => { 11 => {
// Complex number — store as compound of two floats internally // Complex number (HDF5 2.0, datatype version 5). The properties
// Parse like compound with version 3 and 2 members // are a single base floating-point datatype message; an element
// But actually class 11 has no special properties beyond class 6 compound. // is two consecutive base-type values (real, imaginary). There
// It's just recognized as a separate class. For now parse the 2 members // is no member list. Surface it as the equivalent two-member
// as compound. // compound `{r, i}` — the same shape h5py writes for numpy
let num_members = (bf0 as u16) | ((bf1 as u16) << 8); // complex dtypes — so downstream compound readers work as-is.
let mut members = Vec::with_capacity(num_members as usize); if version != 5 {
let ob = offset_bytes_for_size(size); return Err(FormatError::InvalidDatatypeVersion {
for _ in 0..num_members { class: class_id,
let (name, name_len) = read_null_terminated_string(data, pos)?; version,
pos += name_len;
let byte_offset = read_uint(data, pos, ob)?;
pos += ob;
let (member_dt, consumed) = Self::parse_with_depth(&data[pos..], depth + 1)?;
pos += consumed;
members.push(CompoundMember {
name,
byte_offset,
datatype: member_dt,
}); });
} }
let (base_type, consumed) = Self::parse_with_depth(&data[pos..], depth + 1)?;
pos += consumed;
let base_size = base_type.type_size();
if base_size.checked_mul(2) != Some(size) {
return Err(FormatError::DataSizeMismatch {
expected: (base_size as usize).saturating_mul(2),
actual: size as usize,
});
}
let members = vec![
CompoundMember {
name: String::from("r"),
byte_offset: 0,
datatype: base_type.clone(),
},
CompoundMember {
name: String::from("i"),
byte_offset: base_size as u64,
datatype: base_type,
},
];
Ok((Datatype::Compound { size, members }, pos)) Ok((Datatype::Compound { size, members }, pos))
} }
_ => Err(FormatError::InvalidDatatypeClass(class_id)), _ => Err(FormatError::InvalidDatatypeClass(class_id)),
@@ -1126,6 +1138,75 @@ mod tests {
} }
} }
/// Real datatype message bytes emitted by HDF5 2.0 for the native complex
/// type `H5T_COMPLEX_IEEE_F64LE`: class 11, version 5, size 16, followed by
/// the base IEEE f64 datatype message.
const COMPLEX_F64_HDF5_2_0: [u8; 28] = [
0x5b, 0x01, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x11, 0x20, 0x3f, 0x00, 0x08, 0x00, 0x00,
0x00, 0x00, 0x00, 0x40, 0x00, 0x34, 0x0b, 0x00, 0x34, 0xff, 0x03, 0x00, 0x00,
];
#[test]
fn test_complex_v5_from_hdf5_2_0() {
let (dt, consumed) = Datatype::parse(&COMPLEX_F64_HDF5_2_0).unwrap();
assert_eq!(consumed, COMPLEX_F64_HDF5_2_0.len());
match dt {
Datatype::Compound { size, members } => {
assert_eq!(size, 16);
assert_eq!(members.len(), 2);
assert_eq!((members[0].name.as_str(), members[0].byte_offset), ("r", 0));
assert_eq!((members[1].name.as_str(), members[1].byte_offset), ("i", 8));
for m in &members {
assert!(matches!(
m.datatype,
Datatype::FloatingPoint { size: 8, .. }
));
}
}
other => panic!("expected Compound, got {other:?}"),
}
}
#[test]
fn test_compound_with_complex_member_from_hdf5_2_0() {
// Compound { z: complex f64 @0, k: i64 @16 } as written by HDF5 2.0.
// Regression guard: the complex member must consume exactly its own
// bytes so the following member parses.
let mut bytes = vec![0x56, 0x02, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, b'z', 0x00, 0x00];
bytes.extend_from_slice(&COMPLEX_F64_HDF5_2_0);
bytes.extend_from_slice(&[b'k', 0x00, 0x10]);
bytes.extend_from_slice(&[
0x10, 0x08, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00,
]);
let (dt, consumed) = Datatype::parse(&bytes).unwrap();
assert_eq!(consumed, bytes.len());
match dt {
Datatype::Compound { size, members } => {
assert_eq!(size, 24);
assert_eq!(members.len(), 2);
assert!(matches!(
&members[0].datatype,
Datatype::Compound { size: 16, members } if members.len() == 2
));
assert_eq!((members[1].name.as_str(), members[1].byte_offset), ("k", 16));
}
other => panic!("expected Compound, got {other:?}"),
}
}
#[test]
fn test_complex_size_mismatch_rejected() {
let mut bytes = COMPLEX_F64_HDF5_2_0;
bytes[4] = 0x0c; // claims 12 bytes, base type is 8
assert!(matches!(
Datatype::parse(&bytes),
Err(FormatError::DataSizeMismatch {
expected: 16,
actual: 12
})
));
}
#[test] #[test]
fn test_reference_object() { fn test_reference_object() {
let buf = build_dt_header(7, 1, [0, 0, 0], 8); let buf = build_dt_header(7, 1, [0, 0, 0], 8);
@@ -292,6 +292,74 @@ f.close()
assert_eq!(x_vals, vec![1.0, 3.0]); assert_eq!(x_vals, vec![1.0, 3.0]);
} }
#[test]
#[ignore = "requires Python h5py module"]
fn read_h5py_generated_native_complex() {
// HDF5 2.0 native complex (datatype class 11, version 5), written through
// h5py's low-level API. Skips when the linked HDF5 predates 2.0.
let path = std::env::temp_dir().join("clawhdf5_h5py_native_complex.h5");
let gen_script = format!(
r#"
import h5py, numpy as np
from h5py import h5t, h5s, h5d, h5f, h5p
if not getattr(h5py.get_config(), 'has_native_complex', False):
print('SKIP')
else:
fapl = h5p.create(h5p.FILE_ACCESS)
fapl.set_libver_bounds(h5f.LIBVER_LATEST, h5f.LIBVER_LATEST)
fid = h5f.create(b'{}', h5f.ACC_TRUNC, fapl=fapl)
t = h5t.COMPLEX_IEEE_F64LE
d = h5d.create(fid, b'z', t, h5s.create_simple((2,)))
d.write(h5s.ALL, h5s.ALL, np.array([1+2j, 3+4j], dtype=np.complex128), mtype=t)
fid.close()
"#,
path.display()
);
if h5py_read(&path, &gen_script) == "SKIP" {
eprintln!("HDF5 < 2.0: no native complex support, skipping");
return;
}
let bytes = std::fs::read(&path).unwrap();
let sig = clawhdf5_format::signature::find_signature(&bytes).unwrap();
let sb = clawhdf5_format::superblock::Superblock::parse(&bytes, sig).unwrap();
let addr = clawhdf5_format::group_v2::resolve_path_any(&bytes, &sb, "z").unwrap();
let hdr = clawhdf5_format::object_header::ObjectHeader::parse(
&bytes,
addr as usize,
sb.offset_size,
sb.length_size,
)
.unwrap();
let msg = |t: clawhdf5_format::message_type::MessageType| {
&hdr.messages.iter().find(|m| m.msg_type == t).unwrap().data
};
let (dt, _) = clawhdf5_format::datatype::Datatype::parse(msg(
clawhdf5_format::message_type::MessageType::Datatype,
))
.unwrap();
let ds = clawhdf5_format::dataspace::Dataspace::parse(
msg(clawhdf5_format::message_type::MessageType::Dataspace),
sb.length_size,
)
.unwrap();
let dl = clawhdf5_format::data_layout::DataLayout::parse(
msg(clawhdf5_format::message_type::MessageType::DataLayout),
sb.offset_size,
sb.length_size,
)
.unwrap();
let raw = clawhdf5_format::data_read::read_raw_data(&bytes, &dl, &ds, &dt).unwrap();
let fields = clawhdf5_format::data_read::read_compound_fields(&raw, &dt).unwrap();
assert_eq!(fields.len(), 2);
let re =
clawhdf5_format::data_read::read_as_f64(&fields[0].raw_data, &fields[0].datatype).unwrap();
let im =
clawhdf5_format::data_read::read_as_f64(&fields[1].raw_data, &fields[1].datatype).unwrap();
assert_eq!((fields[0].name.as_str(), re), ("r", vec![1.0, 3.0]));
assert_eq!((fields[1].name.as_str(), im), ("i", vec![2.0, 4.0]));
}
#[test] #[test]
#[ignore = "requires Python h5py module"] #[ignore = "requires Python h5py module"]
fn read_h5py_generated_enum() { fn read_h5py_generated_enum() {
+2 -2
View File
@@ -1,10 +1,10 @@
[package] [package]
name = "clawhdf5-gpu" name = "clawhdf5-gpu"
version = "2.1.0" version = "2.2.0"
edition = "2024" edition = "2024"
description = "GPU-accelerated vector operations for rustyhdf5 using wgpu compute shaders" description = "GPU-accelerated vector operations for rustyhdf5 using wgpu compute shaders"
license = "MIT" license = "MIT"
repository = "https://github.com/redclawsystems/clawhdf5" repository = "https://git.redclaw.dev/quantumclaw/clawhdf5"
readme = "README.md" readme = "README.md"
keywords = ["hdf5", "gpu", "wgpu", "compute"] keywords = ["hdf5", "gpu", "wgpu", "compute"]
categories = ["science", "graphics"] categories = ["science", "graphics"]
+3 -3
View File
@@ -1,16 +1,16 @@
[package] [package]
name = "clawhdf5-io" name = "clawhdf5-io"
version = "2.1.0" version = "2.2.0"
edition = "2024" edition = "2024"
description = "I/O abstraction layer for rustyhdf5" description = "I/O abstraction layer for rustyhdf5"
license = "MIT" license = "MIT"
repository = "https://github.com/redclawsystems/clawhdf5" repository = "https://git.redclaw.dev/quantumclaw/clawhdf5"
readme = "README.md" readme = "README.md"
keywords = ["hdf5", "io", "science", "data"] keywords = ["hdf5", "io", "science", "data"]
categories = ["filesystem", "science"] categories = ["filesystem", "science"]
[dependencies] [dependencies]
clawhdf5-format = { path = "../clawhdf5-format", version = "2.1.0" } clawhdf5-format = { path = "../clawhdf5-format", version = "2.2.0" }
memmap2 = { version = "0.9", optional = true } memmap2 = { version = "0.9", optional = true }
libc = { version = "0.2", optional = true } libc = { version = "0.2", optional = true }
tokio = { version = "1", features = ["fs", "io-util"], optional = true } tokio = { version = "1", features = ["fs", "io-util"], optional = true }
+5 -5
View File
@@ -1,10 +1,10 @@
[package] [package]
name = "clawhdf5-migrate" name = "clawhdf5-migrate"
version = "2.1.0" version = "2.2.0"
edition = "2024" edition = "2024"
description = "CLI to migrate SQLite agent memory databases to HDF5 format" description = "CLI to migrate SQLite agent memory databases to HDF5 format"
license = "MIT" license = "MIT"
repository = "https://github.com/redclawsystems/clawhdf5" repository = "https://git.redclaw.dev/quantumclaw/clawhdf5"
readme = "README.md" readme = "README.md"
keywords = ["sqlite", "hdf5", "migration", "agent", "memory"] keywords = ["sqlite", "hdf5", "migration", "agent", "memory"]
categories = ["command-line-utilities", "database"] categories = ["command-line-utilities", "database"]
@@ -14,9 +14,9 @@ name = "clawhdf5-migrate"
path = "src/main.rs" path = "src/main.rs"
[dependencies] [dependencies]
clawhdf5-agent = { path = "../clawhdf5-agent", version = "2.1.0" } clawhdf5-agent = { path = "../clawhdf5-agent", version = "2.2.0" }
clawhdf5-format = { path = "../clawhdf5-format", version = "2.1.0" } clawhdf5-format = { path = "../clawhdf5-format", version = "2.2.0" }
clawhdf5 = { path = "../clawhdf5", version = "2.1.0" } clawhdf5 = { path = "../clawhdf5", version = "2.2.0" }
rusqlite = { version = "0.31", features = ["bundled"] } rusqlite = { version = "0.31", features = ["bundled"] }
clap = { version = "4", features = ["derive"] } clap = { version = "4", features = ["derive"] }
half = { workspace = true } half = { workspace = true }
+3 -3
View File
@@ -1,16 +1,16 @@
[package] [package]
name = "clawhdf5-napi" name = "clawhdf5-napi"
version = "2.1.0" version = "2.2.0"
edition = "2024" edition = "2024"
description = "Node.js native addon (napi-rs) exposing clawhdf5-agent to TypeScript/JavaScript" description = "Node.js native addon (napi-rs) exposing clawhdf5-agent to TypeScript/JavaScript"
license = "MIT" license = "MIT"
repository = "https://github.com/redclawsystems/clawhdf5" repository = "https://git.redclaw.dev/quantumclaw/clawhdf5"
[lib] [lib]
crate-type = ["cdylib"] crate-type = ["cdylib"]
[dependencies] [dependencies]
clawhdf5-agent = { path = "../clawhdf5-agent", version = "2.1.0" } clawhdf5-agent = { path = "../clawhdf5-agent", version = "2.2.0" }
napi = { version = "2", default-features = false, features = ["napi9"] } napi = { version = "2", default-features = false, features = ["napi9"] }
napi-derive = "2" napi-derive = "2"
+4 -4
View File
@@ -1,17 +1,17 @@
[package] [package]
name = "clawhdf5-netcdf4" name = "clawhdf5-netcdf4"
version = "2.1.0" version = "2.2.0"
edition = "2024" edition = "2024"
description = "NetCDF-4 read support built on rustyhdf5 — pure Rust, no C dependencies" description = "NetCDF-4 read support built on rustyhdf5 — pure Rust, no C dependencies"
license = "MIT" license = "MIT"
repository = "https://github.com/redclawsystems/clawhdf5" repository = "https://git.redclaw.dev/quantumclaw/clawhdf5"
readme = "README.md" readme = "README.md"
keywords = ["netcdf", "netcdf4", "hdf5", "science", "climate"] keywords = ["netcdf", "netcdf4", "hdf5", "science", "climate"]
categories = ["parser-implementations", "science"] categories = ["parser-implementations", "science"]
[dependencies] [dependencies]
clawhdf5 = { path = "../clawhdf5", version = "2.1.0" } clawhdf5 = { path = "../clawhdf5", version = "2.2.0" }
clawhdf5-format = { path = "../clawhdf5-format", version = "2.1.0" } clawhdf5-format = { path = "../clawhdf5-format", version = "2.2.0" }
[dev-dependencies] [dev-dependencies]
tempfile = { workspace = true } tempfile = { workspace = true }
+4 -4
View File
@@ -1,10 +1,10 @@
[package] [package]
name = "clawhdf5-py" name = "clawhdf5-py"
version = "2.1.0" version = "2.2.0"
edition = "2024" edition = "2024"
description = "Python bindings for rustyhdf5 — a pure-Rust HDF5 library" description = "Python bindings for rustyhdf5 — a pure-Rust HDF5 library"
license = "MIT" license = "MIT"
repository = "https://github.com/redclawsystems/clawhdf5" repository = "https://git.redclaw.dev/quantumclaw/clawhdf5"
readme = "README.md" readme = "README.md"
keywords = ["hdf5", "python", "bindings", "science"] keywords = ["hdf5", "python", "bindings", "science"]
categories = ["api-bindings", "science"] categories = ["api-bindings", "science"]
@@ -14,8 +14,8 @@ name = "clawhdf5"
crate-type = ["cdylib", "rlib"] crate-type = ["cdylib", "rlib"]
[dependencies] [dependencies]
clawhdf5_rs = { path = "../clawhdf5", version = "2.1.0", package = "clawhdf5" } clawhdf5_rs = { path = "../clawhdf5", version = "2.2.0", package = "clawhdf5" }
clawhdf5-format = { path = "../clawhdf5-format", version = "2.1.0" } clawhdf5-format = { path = "../clawhdf5-format", version = "2.2.0" }
pyo3 = "0.29" pyo3 = "0.29"
numpy = "0.29" numpy = "0.29"
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "maturin"
[project] [project]
name = "rustyhdf5" name = "rustyhdf5"
version = "2.1.0" version = "2.2.0"
description = "Python bindings for rustyhdf5 — a pure-Rust HDF5 library" description = "Python bindings for rustyhdf5 — a pure-Rust HDF5 library"
requires-python = ">=3.8" requires-python = ">=3.8"
license = { text = "MIT" } license = { text = "MIT" }
+7 -7
View File
@@ -1,25 +1,25 @@
[package] [package]
name = "clawhdf5" name = "clawhdf5"
version = "2.1.0" version = "2.2.0"
edition = "2024" edition = "2024"
description = "Pure-Rust HDF5 reader/writer — no C dependencies" description = "Pure-Rust HDF5 reader/writer — no C dependencies"
license = "MIT" license = "MIT"
repository = "https://github.com/redclawsystems/clawhdf5" repository = "https://git.redclaw.dev/quantumclaw/clawhdf5"
readme = "README.md" readme = "README.md"
keywords = ["hdf5", "science", "data", "binary"] keywords = ["hdf5", "science", "data", "binary"]
categories = ["parser-implementations", "science", "encoding"] categories = ["parser-implementations", "science", "encoding"]
[dependencies] [dependencies]
clawhdf5-format = { path = "../clawhdf5-format", version = "2.1.0" } clawhdf5-format = { path = "../clawhdf5-format", version = "2.2.0" }
clawhdf5-io = { path = "../clawhdf5-io", version = "2.1.0" } clawhdf5-io = { path = "../clawhdf5-io", version = "2.2.0" }
rayon = { version = "1", optional = true } rayon = { version = "1", optional = true }
[dev-dependencies] [dev-dependencies]
tempfile = { workspace = true } tempfile = { workspace = true }
criterion = { workspace = true } criterion = { workspace = true }
clawhdf5-io = { path = "../clawhdf5-io", version = "2.1.0", features = ["mmap"] } clawhdf5-io = { path = "../clawhdf5-io", version = "2.2.0", features = ["mmap"] }
clawhdf5-format = { path = "../clawhdf5-format", version = "2.1.0", features = ["parallel", "fast-checksum"] } clawhdf5-format = { path = "../clawhdf5-format", version = "2.2.0", features = ["parallel", "fast-checksum"] }
clawhdf5-filters = { path = "../clawhdf5-filters", version = "2.1.0" } clawhdf5-filters = { path = "../clawhdf5-filters", version = "2.2.0" }
[[bench]] [[bench]]
name = "mmap_bench" name = "mmap_bench"
+1 -1
View File
@@ -556,7 +556,7 @@ let final_results = confidence::reject_low_confidence(
- **[BENCHMARKS.md](../BENCHMARKS.md)** — Full performance numbers - **[BENCHMARKS.md](../BENCHMARKS.md)** — Full performance numbers
- **[ROADMAP.md](../ROADMAP.md)** — What's coming next - **[ROADMAP.md](../ROADMAP.md)** — What's coming next
- **[GitHub](https://github.com/redclawsystems/clawhdf5)** — Source code - **[Source](https://git.redclaw.dev/quantumclaw/clawhdf5)** — Source code
- **[ClawBrainHub](https://clawbrainhub.com)** — The `.brain` marketplace (coming soon) - **[ClawBrainHub](https://clawbrainhub.com)** — The `.brain` marketplace (coming soon)
--- ---
+82
View File
@@ -0,0 +1,82 @@
# Known Issues
Bugs found during development or downstream use, tracked here because this
repository's issue tracker is disabled. One entry per bug; when an entry is
fixed, record the fix in `CHANGELOG.md` and update its status here rather than
deleting it.
---
## Compound datatype message version 5 is not parsed (HDF5 2.0)
**Status:** fixed on `main` in `a13ff51` (2026-06-03); **not in the v2.1.0
tag**, which was cut five commits earlier. Ships in the next release.
**Reported by:** M. Scot Breitenfeld (The HDF Group), 2026-09-08, against v2.1.0.
**Summary:** `clawhdf5-format` v2.1.0 rejects any dataset with a compound
(struct) datatype written by an HDF5 2.0 library in `libver='latest'` mode:
`InvalidDatatypeVersion { class: 6, version: 5 }`.
**Reproduction** (h5py 3.16.0 / HDF5 2.0.0):
```python
import h5py, numpy as np
dt = np.dtype([('x', 'f8'), ('y', 'f8'), ('id', 'i4')])
data = np.array([(1.0, 2.0, 10), (3.0, 4.0, 20)], dtype=dt)
f = h5py.File('compound.h5', 'w', libver='latest')
f.create_dataset('particles', data=data)
f.close()
```
Committed as `crates/clawhdf5-format/tests/writer_h5py_tests.rs::read_h5py_generated_compound`
(`#[ignore]`d; needs `python3` with h5py on `PATH`). Run with
`cargo test -p clawhdf5-format --test writer_h5py_tests -- --include-ignored`:
v2.1.0 gives 25 passed / 1 failed; `main` passes everything.
**Root cause:** the compound (class 6) branch of `Datatype::parse`
(`crates/clawhdf5-format/src/datatype.rs`) accepted only versions 14. Datatype
message versions 4 and 5 changed only the Reference and Complex classes, so a
v5-tagged compound uses the unchanged v3 member-list layout.
**Fix:** versions 35 are accepted for compound (class 6) and array (class 10)
datatypes, and data layout message version 5 is accepted too (needed for every
chunked dataset written by HDF5 2.0). Byte-level regression tests:
`test_compound_v5_from_hdf5_2_0`, `test_array_v5_from_hdf5_2_0`.
## Native complex datatype (class 11) is mis-parsed (HDF5 2.0)
**Status:** fixed 2026-09-18. Found while validating the report above.
**Summary:** HDF5 2.0 native complex types (`H5T_COMPLEX_IEEE_F64LE` etc.)
were parsed as if they carried a compound-style member list. The properties are
actually a single base floating-point datatype, so the parser produced a garbage
datatype, or `UnexpectedEof` when the complex type was a compound member. h5py's
default numpy-complex mapping is unaffected (it writes a `{r, i}` compound);
only files using the native type through the C API / h5py low-level API hit this.
**Fix:** class 11 parses its base type and is surfaced as the equivalent
`{r, i}` compound. Tests: `test_complex_v5_from_hdf5_2_0`,
`test_compound_with_complex_member_from_hdf5_2_0`,
`writer_h5py_tests.rs::read_h5py_generated_native_complex`.
## Revised reference datatype (class 7, version 4) is not parsed
**Status:** open, unconfirmed against a real file.
**Summary:** HDF5 1.12+ `H5T_STD_REF` references use datatype version 4 with
reference types 24 (object2 / region2 / attribute), which `Datatype::parse`
rejects with `InvalidReferenceType`. h5py still writes the legacy v1
object/region references, which read correctly, so no reproducing file has been
generated yet; one written with the C API (`H5T_STD_REF`) is needed.
## `clawhdf5-gpu` `gpu_tests` can hang under the default parallel test runner
**Status:** open. Observed 2026-09-18 (RTX 5060 Ti, Linux).
**Summary:** during `cargo test --workspace`, the `gpu_tests` binary sat idle
(~1% CPU) for 25+ minutes and had to be killed. Run single-threaded it passes
in seconds (20/20): `cargo test -p clawhdf5-gpu --test gpu_tests -- --test-threads=1`.
Suspected cause: several tests creating wgpu devices concurrently (possibly
compounded by the rest of the workspace's tests loading the machine). Not yet
root-caused; workaround is `--test-threads=1` for that crate.
+2 -2
View File
@@ -1,13 +1,13 @@
{ {
"name": "@redclaw/clawhdf5", "name": "@redclaw/clawhdf5",
"version": "2.1.0", "version": "2.2.0",
"description": "Node.js bindings for clawhdf5 — HDF5-backed agent memory with hippocampal consolidation", "description": "Node.js bindings for clawhdf5 — HDF5-backed agent memory with hippocampal consolidation",
"main": "index.js", "main": "index.js",
"types": "index.d.ts", "types": "index.d.ts",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/redclawsystems/clawhdf5" "url": "https://git.redclaw.dev/quantumclaw/clawhdf5"
}, },
"keywords": [ "keywords": [
"agent", "agent",
+201
View File
@@ -0,0 +1,201 @@
# Verification Brief — branch `verify/v3-plus-v6`
Independent audit of three already-implemented fixes:
- **P1** — `clawhdf5-ann::hnsw::compute_distance` now delegates to `clawhdf5-accel`'s
runtime-dispatched SIMD kernels (`l2_distance`, `cosine_similarity`) instead of
scalar loops.
- **P2** — `clawhdf5-io::async_read::AsyncFileReader` now opens the file handle
once and caches it + its length behind a `tokio::sync::Mutex`.
- **PR1** — `clawhdf5-migrate` writes SHINES provenance (`hdf5_writer.rs`) and
verifies it on read-back (`validate.rs`).
Branch state audited: `verify/v3-plus-v6` @ `07b7301` (merge of the v3 ann/io/migrate
work and v6 agent/format work). All three areas' existing test suites
(`cargo test -p clawhdf5-accel -p clawhdf5-ann -p clawhdf5-io --features async
-p clawhdf5-migrate --release`) pass — 41 + 23 + 89 + 26 tests green. That is
expected: the defect below is a numerical edge case none of the existing tests
exercise.
---
## P1 — SIMD distance in `clawhdf5-ann` — DEFECT FOUND
**File:** `crates/clawhdf5-accel/src/scalar.rs`, `avx2.rs`, `avx512.rs`, `neon.rs`
(all four backends share the bug identically; it surfaces in callers through
`crates/clawhdf5-ann/src/hnsw.rs:54`, `compute_distance`'s
`1.0 - clawhdf5_accel::cosine_similarity(a, b)`).
**Problem:** The near-zero-norm guard in `cosine_similarity` changed threshold
during the SIMD migration, and the new threshold is wrong.
Old scalar loop (pre-SIMD, `hnsw.rs` @ `55959b4`):
```rust
let denom = norm_a.sqrt() * norm_b.sqrt();
if denom < f32::EPSILON {
1.0
} else {
1.0 - (dot / denom)
}
```
New code, identical in all four `clawhdf5-accel` backends (e.g.
`scalar.rs:23-24`):
```rust
let denom = (norm_a * norm_b).sqrt();
if denom == 0.0 { 0.0 } else { dot / denom }
```
The old code clamped *any* near-zero denominator (anything under
`f32::EPSILON ≈ 1.19e-7`, not just exact zero) to a safe "maximally
dissimilar" result. The new code only special-cases an **exact** `0.0`
denominator; anything smaller but nonzero falls through to `dot / denom`.
For genuinely-zero vectors the two are equivalent (`denom == 0.0` in both, and
`1.0 - 0.0 == 1.0` matches the old `1.0`), and the existing test
(`hnsw.rs::cosine_zero_vector`, `clawhdf5-accel::test_cosine_zero_vector`)
only covers that case — which is why it didn't catch this.
But for vectors with a small (not exactly zero) norm, the two diverge sharply.
Concrete repro (values confirmed via a standalone build of both functions):
```
a = b = [1e-4] // tiny but nonzero, identical vectors
old cosine distance = 1.0 // "unreliable direction" fallback, correctly
// caps degenerate near-zero vectors at max distance
new cosine distance = 0.0 // computed as fully identical
```
`denom` here is `1e-8`, comfortably below `f32::EPSILON` (`1.19e-7`) but not
`== 0.0`, so the old guard fired and the new one doesn't. This is not a
narrow floating-point-rounding footgun — the divergence spans roughly three
orders of magnitude of vector norm (anything with `denom` in
`(0, 1.19e-7)`), and it flips the result from "maximally dissimilar" to
"identical," the two opposite ends of the distance range. Any HNSW cosine
index that indexes or queries a near-zero-magnitude embedding (e.g. an
embedder's output for empty/masked/degenerate input, or a soft-deleted/
zeroed-out placeholder vector) will silently rank it as a near-duplicate of
other near-zero vectors instead of correctly pushing it to the bottom of
results.
Mismatched-length and truly-empty inputs were also checked: empty vectors
(`a.len() == b.len() == 0`) behave identically old vs. new (both hit the
zero-denominator path → distance `1.0`). Mismatched lengths now panic via
`assert_eq!` in every backend, versus the old code's `for i in 0..a.len()`
(which panicked on OOB if `b` was shorter, or silently truncated to `a`'s
length if `b` was longer). No caller reaches this: `HnswIndex::build_with_metric`
and `insert` both assert equal dimensions before any `compute_distance` call,
so mismatched lengths are unreachable in practice — not flagging as a
separate defect.
**Proposed fix:** Restore the epsilon-threshold guard in all four
`clawhdf5-accel` cosine_similarity backends (`scalar.rs`, `avx2.rs`,
`avx512.rs`, `neon.rs`), replacing `if denom == 0.0 { 0.0 }` with
`if denom < f32::EPSILON { 0.0 }`, so `1.0 - cosine_similarity(...)` in
`hnsw.rs` reproduces the old `denom < f32::EPSILON → 1.0` fallback exactly.
Add a regression test in `clawhdf5-accel` (e.g.
`test_cosine_near_zero_norm_clamped`) asserting `cosine_similarity(&[1e-4],
&[1e-4])` returns `0.0` (so `1.0 - sim == 1.0`, matching the old HNSW
fallback) rather than `1.0`, and a matching test in `hnsw.rs`
(`cosine_near_zero_vector`, alongside the existing `cosine_zero_vector`) using
a tiny-but-nonzero vector pair to lock in `compute_distance == 1.0`.
TASK: INT-01 — Restore f32::EPSILON near-zero-denom guard in clawhdf5-accel cosine_similarity (all 4 backends) + regression tests
---
## P2 — Cached async file handle in `clawhdf5-io` — SOUND, no defect
**File:** `crates/clawhdf5-io/src/async_read.rs`, `AsyncFileReader::read_at` /
`::len` (lines 96-126).
Checked against the pre-fix version (diff in `b08df7b`, which per-call opened
a fresh `tokio::fs::File` and re-stat'd the length):
- **No seek/read interleaving across tasks.** `read_at` takes
`let mut guard = self.handle.lock().await` once at the top and then borrows
`file` from that guard (`guard.as_mut()`) for the rest of the function,
including both the `seek(...).await` and `read_exact(...).await` calls.
Because `file` is a live borrow of `guard`, the Rust borrow checker forces
`guard` (and therefore the lock) to stay held across both await points —
it cannot be dropped until the whole function returns. `tokio::sync::Mutex`
is specifically designed to be held across `.await` (unlike `std::sync::Mutex`),
so a second task's `read_at` call blocks at `.lock().await` until the first
task's seek+read pair has fully completed. A seek from one task can never be
followed by a read from another task on the same descriptor.
- **Lazy-init race is also covered by the same lock.** The `if guard.is_none()`
open-and-populate branch runs under the same guard acquired at the top, so
two concurrent first-callers can't both open+overwrite the cached handle;
the second one to acquire the lock sees `guard.is_some()` and reuses it.
- **Cached length staleness.** The length is cached forever once populated —
intentional and documented in the struct's doc comment ("cached for the
lifetime of this reader"). Grepped the whole workspace
(`AsyncFileReader` outside `async_read.rs` itself): zero other callers exist
yet, so there's no current code path where a caller observes a stale length
against a file that changed size mid-lifetime. If the backing file were
truncated externally during the reader's life, the stale (larger) cached
length would make `read_at` attempt to read more than remains on disk —
but that fails loudly via `read_exact`'s `UnexpectedEof` rather than
silently returning corrupted/truncated data, which is a safe failure mode,
not a correctness bug.
- **Short-read/truncation semantics.** The `offset >= file_len → empty`,
`to_read = len.min(available)` logic is byte-for-byte unchanged from the
pre-fix version; only the source of `file_len` changed (cached vs.
freshly stat'd). For the current, only-consumer-is-itself usage pattern
(open once, read many times, file not mutated externsally during the
reader's life) the observable behavior is identical to before.
No item raised for P2.
---
## PR1 — SHINES provenance in `clawhdf5-migrate` — SOUND, no defect
**Files:** `crates/clawhdf5-migrate/src/hdf5_writer.rs`,
`crates/clawhdf5-migrate/src/main.rs`, `crates/clawhdf5-migrate/src/validate.rs`,
`crates/clawhdf5-migrate/src/hdf5_reader.rs`.
- **Current-run source path / timestamp on `--incremental` merges.**
`write_hdf5` (`hdf5_writer.rs:23`) computes `timestamp = iso8601_now()`
fresh on every call — it is never read from the merged `data` struct, so
the top-level `migrated_at` attribute and the per-dataset
`.with_provenance("clawhdf5-migrate", timestamp, source_opt)` calls
(`hdf5_writer.rs:147,177,189`) always carry the current run's wall-clock
time, incremental or not. For `source_path`: `hdf5_reader::read_hdf5`
(used to load the incremental base) explicitly returns
`source_path: String::new()` with a comment noting the caller must carry
the real path forward (`hdf5_reader.rs:52-56`); `main.rs:160`
(`base.source_path = source.source_path`) does exactly that — it
overwrites the re-read base's placeholder with the *freshly re-read SQLite
source's* path before calling `write_hdf5`, not a previous run's path.
Traced through: on an `--incremental` run, both the top-level attributes
and every per-dataset provenance attribute reflect the current run, not a
stale one. `test_incremental_migration` (`main.rs`) exercises the merge
path and passes, though it doesn't assert on `source_path`/`migrated_at`
specifically — the coding phase could add that assertion as cheap
extra insurance, but it's not fixing a defect, just tightening coverage.
- **Hash-mismatch vs. absent-attribute handling.**
`verify_chunk_provenance` (`validate.rs:161-184`) returns `Err(...)`
(fails loudly, wired through `validate_hdf5`'s `?`) only on
`VerifyResult::Mismatch`, i.e. an actual recomputed-vs-stored SHA-256
disagreement. `VerifyResult::NoHash` (attribute absent, e.g. an
older output file) is handled separately — it sets `all_present = false`
and continues, returning `Ok(false)` from `verify_chunk_provenance`
(surfaced as `ValidationSummary::provenance_verified == false`, not an
error). This is correctly asymmetric: real corruption is a hard error,
merely-missing provenance metadata is a soft "unverified" signal, matching
the documented contract in the function's doc comment.
No item raised for PR1.
---
## Summary
| Item | Verdict | Follow-up |
|------|---------|-----------|
| P1 SIMD distance | **Defect** — cosine near-zero-norm guard weakened from `< f32::EPSILON` to `== 0.0` across all 4 backends | INT-01 |
| P2 async file handle | Sound | none |
| PR1 migrate provenance | Sound | none |