Skip to content

Update the branch to date - #80

Merged
zaoxing merged 204 commits into
alan-reviewfrom
main
Aug 3, 2026
Merged

Update the branch to date#80
zaoxing merged 204 commits into
alan-reviewfrom
main

Conversation

@zaoxing

@zaoxing zaoxing commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

No description provided.

GordonYuanyc and others added 30 commits February 26, 2026 17:09
add wrapper for CMS+HEAP and CS+HEAP
GordonYuanyc and others added 29 commits July 18, 2026 23:24
Carry the full 20-seed list in HLL/CMS metadata (not just hash_profile_id), so a
consumer can read the exact seeds and algorithm straight from the bytes with no
registry. ~130 bytes/sketch; resolving seeds from the profile id alone is left as
a v2 space optimization. Doc + fail-closed unknown-key test updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Apply `cargo clippy --fix` for the uninlined_format_args lint across all
targets (lib + tests) so the CI clippy step passes with
`--all-targets --all-features -D warnings`. Purely mechanical
`format!("{}", x)` -> `format!("{x}")` rewrites; no logic changes.

Also update examples/serialize_deserialize.rs to use an i64 CMS counter,
since the CMS wire format now constrains counters to CmsWireCounter
(i64/f64); this restores compilation of the --all-targets build.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
HLL and Count-Min `serialize_to_bytes` were generic over the hasher but
hardcoded the standard ProjectASAP profile into the metadata, so a sketch
built with a custom hasher would serialize metadata that lied about how it
was hashed. Introduce a `HashProfile` trait alongside `SketchHasher` and
derive the wire metadata from it:

- New `HashProfile` trait in `common/hash.rs`, implemented for
  `DefaultXxHasher` with the exact standard-profile values (now the single
  source of truth; the envelope `HASH_*` string constants are removed).
- HLL: `standard_hll_metadata` is now `hll_metadata::<DefaultXxHasher>`; the
  generic wire methods build/validate `hll_metadata::<H>` and are bounded on
  `H: HashProfile`. HIP stays on `DefaultXxHasher`. Portable path unchanged.
- Count-Min: `cms_metadata::<H>` reads the profile (incl. `MATRIX_SEED_INDEX`);
  wire methods and the native `MessagePackCodec` impl gain `H: HashProfile`.

Unprofiled hashers become impossible to serialize (compile error → fail
closed). The standard-profile output is byte-identical: all existing
round-trips, `native_and_portable_hll_bytes_match`, and the Go-parity
goldens still pass. Adds custom-hasher (`AltHasher`) round-trip tests for
HLL and CMS verifying distinct, self-describing bytes and that a
standard-profile decode rejects custom-profile bytes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…handling

The ASAPv1 hash-spec metadata is now derived from the hasher's HashProfile
(hll_metadata::<H> / cms_metadata::<H>) rather than hardcoded. Document that
the field values are sourced from the hasher (standard profile = DefaultXxHasher),
that custom hash profiles are supported and self-describing, and that the format
fails closed on both ends (serialize requires H: HashProfile; decode validates
against the target type's profile). Add a Custom hash profiles subsection, clarify
Section 2 validation, add a Q-PROFILE decision, and note Go-side handling.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…nfigs)

Add "Section 4 — Wire coverage" to docs/asapv1_wire_format.md making the
coverage decision explicit: which in-memory HLL and Count-Min configs are
wire-eligible, and the concrete function/trait to implement (or conversion to
do) for the ones that are not. Includes an HLL coverage table (all variants,
precisions, and any H: HashProfile), a Count-Min coverage table, an actionable
"If you want X, do Y" table, and the rationale for keeping the wire a small
fixed set. Renumber the former Section 4 (Wire encoding rules) to Section 5 and
update the two cross-references in Decisions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ic_ids

Align the Section 1 kind_id registry family bytes to the ids already
committed in sketchlib-go's wire/asapmsgpack/magic_ids.go, correcting the
earlier speculative KLL/DDSketch/KMV/CountSketch assignments that conflicted
with Go. Extend the registry with new family bytes (0x0a+) for the remaining
sketches inventoried in docs/apis.md, add a Status column, and add mapping
notes for the CMSHeap/CSHeap, Hydra, and SetAggregator/DeltaResult cases.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CI's stable clippy is newer than the local toolchain and flags
`redundant reference in println! argument` at countsketch.rs:470
(`row_slice` already returns a slice). Remove the extra & (and inline
the format arg). Was the only newer-clippy error under -D warnings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… notes

- Mermaid erDiagram (Structure/entity view) contrasting ASAPv1's self-contained
  one-to-one shape with OTel-Arrow's normalized one-to-many fan-out
- placeholder payload subsections for every registry kind_id (§3.3–§3.22), with
  per-sketch design notes
- registry: KLL-dynamic row; 'kind_id = algorithm level' framing note

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Review findings on PR #65:
- CMS deserialize panicked on a crafted zero-dimension payload ([rows,0,[]])
  via Vector2D::from_fn -> 0.ilog2(); now rejected with an Err before from_fn
- portable HLL decode used 1<<precision (shift-overflow panic on crafted
  precision); now checked_shl -> Err
- add negative tests: CMS zero-dim rejection, CMS unknown-key rejection,
  HLL precision cross-rejection (P12 bytes vs P14 decoder), HIP kind_id
  rejected by a Classic decoder

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add shared ASAPv1 golden byte-vectors that machine-prove the Rust and Go
wire encodings are byte-identical, closing the "no cross-language goldens"
gap both code reviews flagged.

Each fixture is built from a fixed, KNOWN raw sketch state (register bytes
/ matrix values set directly, never hashed), so the golden tests the wire
encoding in isolation from the hash functions. Fixtures cover the
implemented wire configs: HLL Classic/Ertl-MLE/HIP at P12, Count-Min
i64/RegularPath and f64/FastPath.

The .hex goldens (asapv1_golden/) are authored by the Rust reference
encoder and are checked in byte-identically to sketchlib-go/asapv1_golden/.
tests/asapv1_golden.rs builds each fixture from known state, serializes,
and asserts == golden, plus asserts deserialize(golden) round-trips.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Count-Min's matrix dimensions are configuration that shapes the payload
(like HLL's precision), so per the spec's config→metadata rule they now
live in the descriptor metadata instead of the payload.

- CmsMetadata gains `rows: u32` / `cols: u32` as structural params.
  Canonical field order (the wire contract Go must mirror):
  metadata_version, hash_profile_id, hash_algorithm, seed_derivation,
  input_encoding, seed_list, matrix_seed_index, rows, cols,
  counter_type, mode.
- cms_metadata::<H>(..) now takes rows/cols and populates them.
- CmsPayload drops rows/cols; the payload is now a 1-element positional
  array `[counts]` (mirroring HLL Classic's `[registers]`).
- decode reads rows/cols from the validated metadata; the zero-dimension
  guard and the counts.len() == rows*cols check are unchanged.
- Regenerated the two CMS goldens (cms_i64_regular_2x3.hex,
  cms_f64_fast_2x3.hex). The three HLL goldens are unchanged.
- Updated docs/asapv1_wire_format.md (§2, §3.2, §5, Decisions) and added
  a "Code organization" note documenting the mod/wire split.

NOTE: the Go copies of the two CMS goldens now differ from Rust and will
stay mismatched until Go PR #70 makes the matching rows/cols → metadata
change. That is expected and a follow-up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- §2 Fields: add an overview table (metadata = Hash spec + Structural params,
  with each group's role and member fields) above the two detail tables
- §2 Encoding: replace the loose 'optional / omit the key' wording (and the
  incorrect 'unknown keys are skippable', which contradicts deny_unknown_fields)
  with the accurate model: each sketch has its own fixed metadata schema; every
  field is required within it; missing/extra keys fail closed
- mermaid METADATA entity: annotate the hash-spec vs structural-params grouping

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add §5 Implementation detail (move Validation + the exotic-sketch
conversion recipe out of §2/§3 into it). Reflow the whole doc to
one-sentence-per-line. Tighten Status to bullets, lead with Layering,
drop the redundant three-way overview, trim the registry history, and
fix a §3.2 inaccuracy (both FastPath/RegularPath serialize directly; a
mode is only 'converted' to change it).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Add "What this is" intro and "Which parts to read" reading guide
- Add a "Terms" section (envelope, metadata, payload, kind_id, hash
  profile, seed_list, wire-eligible) so terms are defined before use
- Clarify the Layering "who authors each part" paragraph (envelope and
  metadata are authored once but shipped with every sketch)
- Replace the structure diagram with an erDiagram: SerializedSketch as an
  ordered envelope/metadata/payload container, payload fanning out per kind
- Redraw the layering text box in ASCII; drop redundant caption and the
  weak "by goal" reading list

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Replace all non-ASCII characters with ASCII equivalents (em/en dashes,
  section signs, arrows, checkmarks, sigma/superscript, <=, etc.)
- Rephrase "rather than" and "X, not Y" constructions as positive statements
- Drop "2-byte" from the kind_id prose and mark [family, variant] as today's
  layout, since kind_id is variable-length (kind_id_len is a u8)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…table

- Cut redundant restatements of seed_list-inlined, fail-closed, and
  golden-vectors-lock across Section 2/4/5, Cross-language, and Decisions
  (keep one canonical statement of each)
- Collapse the 20 not-yet-designed payload subsections (3.3-3.22) into a
  single reference table
- Remove the deferred "Wire coverage" HTML-commented block

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
feat(wire): ASAPv1 self-describing wire format (envelope + HLL + Count-Min)
…ss-language framing)

Rewrite message_pack_format.md to describe the shared envelope.rs framing,
per-sketch <sketch>/wire.rs serialization, and HashProfile-derived metadata,
and to mark portable/native as deprecated. Point every wire-touching doc at
docs/asapv1_wire_format.md as the single source of truth. Note the
serialize helpers are bounded on wire-eligible configs (HLL: HllWireVariant
+ HashProfile; CMS: CmsWireCounter/CmsWireMode + HashProfile).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
docs: align existing docs/ pages with the ASAPv1 wire format
CountMin::estimate on the regular path seeded the running minimum with
S::Counter::from(i32::MAX). For counters wider than i32 (i64/i128), any
key whose probed cells all exceed 2147483647 never triggered the
`v < min` update, so estimate silently returned the seed — capping every
large count at exactly 2147483647.

Seed the minimum from row 0's probed cell instead, matching the fast
path's fast_query_min. Add a regression test covering an i64 sketch with
counts of ~35 billion.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
fix(cms): stop clamping RegularPath estimates at i32::MAX
Fixes a real correctness gap surfaced while implementing
ASAPQuery-backend's SummaryExecutor (data_plane): the only
heap-bearing frequency wire type this crate exposed was
CountMinSketchWithHeap (portable::countminsketch_topk). data_plane's
query-side reducer reused that same type for BOTH CmsWithHeap and
CountSketchWithHeap sids -- meaning every CountSketchWithHeap query was
being read out with CMS's min-over-rows estimator instead of Count
Sketch's own median-of-signed-rows estimator. The sketches::CSHeap
computational type (median estimator, "mirrors CMSHeap but with Count
Sketch") already existed and was already correct; there was just no
portable/wire-format wrapper for it, unlike every other sketch family.

Adds `message_pack_format::portable::countsketch_topk::CountSketchWithHeap`,
structurally mirroring CountMinSketchWithHeap (same wire shape: {sketch:
{sketch, row_num, col_num}, topk_heap: [{key, value}], heap_size}) but
backed by CSHeap instead of CMSHeap. The wire SHAPE is unchanged from
what CountMinSketchWithHeap already produces/consumes for this data --
this is a new Rust-side reader/builder for the same bytes with correct
math, not a new cross-language protocol; sketchlib-go needs no changes.

Also adds `#[derive(Clone)]` to `sketches::CSHeap` (CMSHeap already had
it; CSHeap was missing it, needed for CountSketchWithHeap::merge_refs's
structural clone -- both of CSHeap's own fields, Count and HHHeap, are
already Clone).

## Test plan
- [x] `cargo test message_pack_format::portable::countsketch_topk` -- 8
      new tests, all passing: creation, empty query, merge (+ dimension
      mismatch), msgpack round-trip, aggregate_topk (+ empty), and
      test_median_estimator_differs_from_cms_style_min -- the actual
      point of this addition, proving the median estimator behaves
      differently from a CMS-style min estimator on a skewed insert
      pattern, not just sharing a struct shape.
- [x] `cargo test` (full workspace) -- 466 unit + 5 asapv1_golden + 12
      msgpack_compat (5 ignored) + 4 sketches_go_parity_probe + 1
      xtest_consumer + 19 doctests, all passing.
- [x] `cargo clippy --all-targets --all-features -- -D warnings` --
      clean.
- [x] `cargo fmt --check` -- clean.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Add self-describing ASAPv1 serialization for the two KLL quantile
sketches, mirroring the HLL/CMS pattern: compact `KLL` (kind_id
`0x06 0x00`) and `KLLDynamic` (`0x06 0x01`) share one payload
`[levels, items, coin]`, with `serialize_to_bytes` /
`deserialize_from_bytes` living in per-sketch `wire` submodules.

KLL never hashes (it orders raw values with `total_cmp`), so its
metadata carries no hash-spec group — only the structural params
`{metadata_version, k, m, item_type}` (decision Q-KLL). `item_type`
(`f64`/`i64`) is a metadata param, not a separate kind_id (mirrors
CMS `counter_type`). Retained samples use the top-most-level-first
layout that matches sketchlib-go's `KLLState`, so the compact KLL's
leftward-grown L0 is reversed to input order on the wire.

Decode fails closed: kind_id + metadata + item_type checks, level-
layout consistency, coin `remaining_bits <= 64`, and a `2 <= m <= k
<= MAX_CACHEABLE_K` bound so crafted `k`/`m` cannot drive
`compute_max_capacity` into a huge allocation. The nested serde
`Serialize`/`Deserialize` on `KLL` (used by `HydraCounter`) is kept.

Add golden byte-vectors `kll_f64_k200` / `kll_i64_k200` (1..=50,
seed 42, no compaction — deterministic) and cross-language tests;
document the payload in docs/asapv1_wire_format.md §3.3.

KLLDynamic golden is deferred (it has no seeded constructor); the
portable `KllSketch` double-wrap and the Go mirror are follow-ups.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Second-review follow-up. Two decode-side robustness gaps:

1. The `k`/`m` bound added to the ASAPv1 decoder was missing on the
   retained nested serde `Deserialize` for `KLL` (the `HydraCounter`
   path, reachable via `Hydra::deserialize_from_bytes` and direct
   `rmp_serde::from_slice::<KLL<_>>`). A crafted `k` near `usize::MAX`
   overflowed `compute_max_capacity` / drove a huge allocation. Apply
   the same `2 <= m <= k <= MAX_CACHEABLE_K` bound plus level-layout
   validation (so the buffer math can't underflow or index OOB).

2. `validate_kll_payload` (both ASAPv1 variants) accepted a
   structurally-valid but adversarial level distribution — e.g. many
   items parked at a high compactor level — that decoded fine but then
   overflowed the weighted `count()`/`rank()`/`cdf()` (`size * 2^h`) at
   query time. Add a shared `checked_weighted_count` guard so decode
   rejects any layout whose weighted count would overflow; a live
   sketch's weighted count equals its ingested item count, so real
   bytes are never rejected.

Adds regression tests for both (nested-serde crafted `k`, ASAPv1
weighted-count overflow). Full suite + hydra green; clippy clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add an optional `seed` key to the KLL ASAPv1 metadata so a decoded
sketch keeps clear()-reproducibility instead of falling back to
wall-clock. It's construction config, so it lives in the metadata
(config→metadata rule), and it's the first optional key in v1:
present only when the sketch carries a seed (Some), omitted otherwise
(skip_serializing_if + default). The payload's `coin` already carries
the RNG's current position (enough to resume compaction); `seed` is
what a later clear() re-seeds from.

Scope: compact KLL populates and restores it. KLLDynamic has no seed
concept and always omits the key — the two variants are deliberately
not forced to be symmetric here. A consumer that doesn't use the key
(including Go) must still preserve it verbatim on re-encode.

Regenerate the kll_f64/kll_i64 goldens (metadata gains seed=42) and
add tests: seed present-when-seeded / omitted-when-unseeded, and that
it survives a round trip so a decoded sketch's clear() stays
deterministic. Document as decision Q-KLL-SEED.

Relates to determinism gap tracked in #77 (tumbling KLL uses unseeded
init) — that fix is separate (framework-side).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…table

Closes a coverage gap from the third review: existing positive
round-trip tests all used init_kll_with_seed, so the seed-absent
metadata path was exercised on encode but never fully decoded and
re-serialized. Add an unseeded decode + byte-stable re-serialize
assertion.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The KLL payload section carried a note contrasting the new format with
the proto `KLLState` (`proto/kll/kll.proto`). Two reasons to remove it:

- `KLLState` is on the way out — it is exactly the layer this ASAPv1
  path replaces (SketchEnvelope field 13, hydra.proto, and portable
  `from_portable_state`). A doc describing the new format should not
  document the format it retires.
- The note described `KLLState` as the "delta-transmission" path, which
  is not accurate: KLL has no delta proto at all (the delta codec covers
  DDSketch/HLL/CMS/CountSketch only).

The item-order contract above it is unchanged — it is the normative
layout spec (index mapping, level 0 in input order, the compact KLL's
leftward-growth reversal, and the intra-level ordering caveat), and its
reference to sketchlib-go's `KLLState` names the Go struct the
cross-language byte layout is anchored to, not the proto format.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
feat(kll): ASAPv1 wire payload for KLL and KLLDynamic
@zaoxing
zaoxing merged commit b4b3b19 into alan-review Aug 3, 2026
3 checks passed
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.

5 participants