Skip to content

Commit 38dfa22

Browse files
marius-bughiuclaude
andcommitted
docs(changelog): add [2.2.0] section; collapse duplicate [Unreleased] Added (#251)
v2.2.0 was tagged and released (2026-07-05) but CHANGELOG.md had no `## [2.2.0]` section — everything that shipped in it was stranded under `[Unreleased]`, alongside genuinely-unreleased post-2.2.0 work, and the `[Unreleased]` block carried two `### Added` subsections (a Keep-a-Changelog hygiene violation and the visible symptom of the release rollover never happening). Split at the real release boundary (verified via `git log v2.1.0..v2.2.0` and `git log v2.2.0..HEAD`): - New `## [2.2.0] - 2026-07-05` (tag date) with a single `### Added`: CelerityMultiSet (#235), SwissSet, TopKSketch (#238), and the ISet<T> set-algebra surface (#240). - BitWriter/BitReader (#242) — which landed *after* the v2.2.0 tag — plus RobinHoodSet, HashCachingSet, PooledCeleritySet, and all newer work stay under `[Unreleased]`, now collapsed into a single `### Added` block. No change needed to release.yml: its extraction step already keys off the `## [<version>]` heading and fails loudly on an empty section, so the root cause was purely the missing heading. Verified the awk now emits the four v2.2.0 entries for [2.2.0] and stops cleanly at [2.1.0]. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 690f207 commit 38dfa22

1 file changed

Lines changed: 4 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,15 +106,17 @@ All notable changes to Celerity are documented here. This project follows [Keep
106106

107107
(Parity note: `ROADMAP.md` carries no `RobinHoodSet` item to flip — milestone 2.0.0's Robin Hood probing work is already `done`, and it shipped the strategy as `RobinHoodDictionary` only. This closes the dictionary→set parity gap discovered while reading the source after the roadmap was otherwise exhausted, exactly the drift the parity checklist exists to prevent — the same pattern as the `SwissSet` addition. Facets that genuinely do not apply: **no AOT smoke-test entry** — `Celerity.AotSmokeTest` exercises the core sets (`IntSet` / `LongSet` / `CeleritySet`) but not the specialized `SwissSet` / `RobinHoodSet` peers, so this follows the established `SwissSet` precedent.)
108108

109-
### Added
110-
111109
- **`BitWriter` / `BitReader`** in `Celerity.Primitives` — a **sequential, bounds-safe pair of `ref struct` cursors for packing and unpacking arbitrary-width bit fields** over a caller-owned `Span<byte>` / `ReadOnlySpan<byte>`, with no stream and no allocation. They are the **sequential, sub-byte** counterpart to the existing bit primitives: `VarInt` is byte-granular (whole-byte variable-length integers) and `SpanBits` is random-access (get/set one bit at a fixed index over a `Span<ulong>`), whereas `BitWriter` / `BitReader` append and consume **whole multi-bit fields at a moving cursor** — a 3-bit flag group, a 12-bit sample, a 20-bit offset — so a record of odd-width fields occupies exactly `ceil(total_bits / 8)` bytes instead of one byte per field. **The documented BCL-gap workload is bit-packed serialization** (wire protocols, compression bitstreams, packed columnar / bitmap-index encodings, fixed-width-field records): `System.Collections.BitArray` is a heap object that sets one bit at a time and cannot append a multi-bit field, and `System.Buffers.Binary.BinaryPrimitives` is byte-granular, so there is no span-based multi-bit bit writer in the BCL. Bit order is **LSB-first** (little-endian bits, the DEFLATE convention), and the writer and reader are exact inverses when fields are read back in the same order and widths. Surface: `BitWriter(Span<byte>)` with `TryWriteBits(ulong value, int bitCount)` / `TryWriteBit(bool)` / `BitsWritten` / `BytesWritten` / `CapacityInBits` / `BitsRemaining` and a static `ByteCount(int)` buffer-sizing helper; `BitReader(ReadOnlySpan<byte>)` with `TryReadBits(int bitCount, out ulong)` / `TryReadBit(out bool)` / `BitsRead` / `BitsRemaining`. Every `TryWrite` / `TryRead` is bounds-safe (returns `false` and leaves the cursor and buffer unchanged rather than writing or consuming a partial field), each write stores only the low `bitCount` bits (an out-of-range value can never corrupt a following field) and overwrites exactly the bits it occupies (no pre-zeroing required), a `0`-bit field is a no-op success, and a `bitCount` outside `[0, 64]` throws `ArgumentOutOfRangeException`. Filed and shipped after the roadmap was otherwise exhausted (a tier-(c) primitives-gap enhancement, in a different lane from the recent set/sketch collection work).
112110
- `BitBufferTests` (`Celerity.Tests/Utils`) — dedicated coverage mirroring `VarIntTests` / `SpanBitsTests`: `ByteCount` rounding + negative-throws, a mixed-width (3/12/1/20/28-bit) field-record round-trip, the observable LSB-first byte layout, high-bits-above-width discarded (no leak into the next field), full-width (1/8/16/31/32/63/64-bit) values round-tripped at every sub-byte start offset, the single-bit `TryWriteBit` / `TryReadBit` helpers, the zero-width no-op, the bounds-safe overfull-write / truncated-read failures (asserting the cursor and earlier bytes are unchanged), `bitCount` argument validation, clear-then-set correctness over a dirty (`0xFF`-filled) buffer, and a 3,000-trial randomized reconciliation that packs a random field plan and checks the buffer bit-for-bit against a `bool[]` model then reads every field back masked to its width. Also exercised by the Native AOT smoke test (`Celerity.AotSmokeTest`), which round-trips a mixed-width record and confirms the overfull-write guard on the native runtime.
113111
- `BitPackingBenchmark` in `Celerity.Benchmarks`, registered in `Program.cs`'s `ExtendedBenchmarks` array (the extended, on-demand suite, mirroring `VarIntBenchmark` — an isolated microbenchmark kept out of the per-PR core regression gate) — `[MemoryDiagnoser]` `Pack` / `Unpack` categories over a 4,096-field stream of 1–32-bit values, baselined against the idiomatic BCL bit-packing path (a reused `System.Collections.BitArray` set one bit at a time, then `CopyTo` the backing bytes), so the ratio reads as the span bit codec relative to the `BitArray` path.
114112
- Documentation for `BitWriter` / `BitReader`: a full API section in [`docs/api/utilities.md`](docs/api/utilities.md#bitwriter--bitreader-sequential-sub-byte-bit-io) (the LSB-first bit-order convention, the field-cursor mechanism, the `ref struct` constraints, the method surface, the bounds-safety and high-bit-masking contract, and a runnable pack/unpack example) mirroring the `SpanBits` / `VarInt` sections, plus a README entry in the `Celerity.Primitives` walkthrough with a runnable example positioned alongside `VarInt` and `SpanBits`.
115113

116114
(Parity facets that genuinely do not apply: **no gh-pages dashboard wiring** — the dashboard `COLLECTIONS` arrays and ship cards are for the *collection* types; the `Celerity.Primitives` utilities, `BitPackingBenchmark` included, are extended-suite microbenchmarks not surfaced on the per-commit collections dashboard, exactly like `VarIntBenchmark` / `SpanBitsBenchmark`. **No cross-collection shared tests** — `BitWriter` / `BitReader` are not open-addressed set/dict types, so the family-wide `Add` / `TryAdd` / `SetConstructorValidation` / `IEnumerableConstructor` suites do not apply, as they do not for `VarInt` / `SpanBits`. **No `ROADMAP.md` status flip** — milestone 2.1.0's `Celerity.Primitives` bit/span work is already `done` and shipped `VarInt` + `SpanBits`; this adds the sequential sub-byte field cursor that sits between them, after the roadmap was otherwise exhausted, the established post-roadmap tier-(c) pattern.)
117115

116+
## [2.2.0] - 2026-07-05
117+
118+
### Added
119+
118120
- **`TopKSketch<T, THasher>`** in `Celerity.Collections` — a **space-bounded top-k / heavy-hitters sketch** implementing the **Space-Saving** algorithm (Metwally, Agrawal & El Abbadi, 2005), completing the streaming-sketch family: **membership** (`BloomFilter`, `CuckooFilter`) → **cardinality** (`HyperLogLog`) → **frequency** (`CountMinSketch`) → **top-k** ([#238](https://github.com/marius-bughiu/Celerity/issues/238)). It reports a stream's most frequent elements from a fixed `Capacity` of *monitors* (element / count / error triples), so its memory is `O(k)` regardless of stream cardinality — **the documented BCL-beating workload is top-k over a high-cardinality stream**, where a `Dictionary<T,int>` must materialize *every distinct key* just to rank the top few (`O(distinct)` memory), whereas the sketch holds only `k` monitors. Guarantees: it **never underestimates** a monitored count, never misses an element whose true frequency exceeds `TotalCount / Capacity`, and bounds each element's true frequency to `[Count − Error, Count]`. The monitors live in an indexed binary **min-heap** keyed on count (the next eviction victim at the root) so both a repeat observation and an eviction are `O(log k)`, and the element→monitor index **dogfoods `CelerityDictionary<T, int, THasher>`** — which is where `THasher` is used and which supplies the out-of-band `default(T)` / `null` handling for free (a string hasher is never invoked with `null`). Surface: `Add(item)` / `Add(item, count)` (positive-count-validated, `long`-saturating), `TryGetCount(item, out count, out error)`, `GetTopK()` / `GetTopK(int)` (returning `TopKEntry<T>` — element / count / error — sorted by count descending), `Clear`, and `Capacity` / `Count` (distinct monitored) / `TotalCount` (stream length) properties, plus an `IEnumerable<T>` counting constructor. It is **add-and-query only**: like a Bloom filter it has no `Remove`, and unlike `CountMinSketch` / `HyperLogLog` it deliberately has **no `UnionWith`** — two bounded top-k summaries have no exact merge, so no lossy one is offered (documented). A new public readonly struct **`TopKEntry<T>`** carries a result's `Element` / `Count` / `Error`. Filed and shipped after the roadmap was otherwise exhausted (a tier-(c) enhancement), mirroring `CelerityMultiSet` ([#235](https://github.com/marius-bughiu/Celerity/issues/235)).
119121
- `TopKSketchTests` and `TopKSketchAccuracyTests` (`Celerity.Tests/Collections`) — dedicated coverage mirroring the `CountMinSketch*` / `HyperLogLog*` sketch tests. The behaviour suite pins the core surface (empty-sketch zeros; within-capacity exact counts; the eviction path that hands the newcomer the evicted minimum as its error floor incl. the `Capacity == 1` degenerate case; `GetTopK` ordering / truncation / validation; `TryGetCount`; `Clear`-then-reuse; capacity and null-source-beats-capacity constructor validation; and the out-of-band `default(int)` zero and `null`-string elements routed through the dogfooded dictionary). The accuracy suite asserts the hard Space-Saving theorems that hold for **every** input regardless of eviction tie-breaking — exactness (and `Error == 0`) when the capacity is not exceeded, reconciled against a `Dictionary` count-and-sort oracle; never-underestimate and the `[Count − Error, Count]` frequency interval for every survivor under heavy Zipfian eviction; genuine heavy hitters (frequency `> TotalCount / Capacity`) never missed; top-1 correct when one element dominates; and `GetTopK` sorted descending.
120122
- `TopKSketchBenchmark` in `Celerity.Benchmarks`, registered in `Program.cs`'s `CoreBenchmarks` array (so it joins the per-PR core run and the gh-pages dashboard, mirroring `CountMinSketchBenchmark`) — `[MemoryDiagnoser]` `TopKSketch<int, Int32WangHasher>(k=100)` vs a `Dictionary<int,int>` counted in full and then `OrderByDescending`-sorted to extract the top-k, over a high-cardinality stream with planted heavy hitters across `[Params(1000, 100_000)]`, split into `Add` (build) and `TopK` (extract) categories so the Allocated column shows the sketch's constant `O(k)` footprint against the dictionary's `O(distinct)` growth.

0 commit comments

Comments
 (0)