feat: fjall adapter, store refactoring, serde codecs, repository builder#138
Merged
Conversation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Implement Tasks 3-5 for the nexus-fjall crate: - FjallError enum with Io, CorruptValue, and CorruptMeta variants satisfying Error + Send + Sync + 'static - FjallStream implementing EventStream trait as a lending cursor over eagerly-loaded fjall range scan results, with debug_assertions monotonicity checking - FjallStore struct holding TxKeyspace, two TxPartitionHandles (streams + events), and AtomicU64 counter for stream ID allocation - FjallStoreBuilder with configurable partition options and recovery of next_stream_id on reopen Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Implement the RawEventStore trait for FjallStore with atomic append (transactional version check + insert + commit) and range-scan-based read_stream. Remove dead_code allows from encoding functions and FjallStore struct now that they are used. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add 6 conformance tests: data persistence across reopen, stream ID counter recovery, version tracking, large batch (100 events), schema version round-trip, and empty batch no-op. All 33 tests pass with clippy --deny warnings and fmt clean. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- C1: temp_store() now returns (FjallStore, TempDir) so TempDir lives until each test ends; all callers destructure as (store, _dir) - S2: Use PersistedEnvelope::try_new() instead of new() in FjallStream to avoid panic on corrupt data with schema_version == 0 - S4: Early return in append() when envelopes slice is empty to avoid creating phantom stream entries - I3: Added comment explaining why Relaxed ordering is safe for fetch_add on next_stream_id Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…sting 42 tests across 10 attack categories: encoding roundtrips, append-read integrity, evil stream IDs, version boundary arithmetic, concurrency races, persistence/recovery, stress/scale, schema version edges, model-based shadow store, and degenerate cases. BUG FOUND: empty string stream ID causes panic in fjall's lsm-tree (key may not be empty). FjallStore::append should validate non-empty stream_id before passing to fjall. Documented in test attack_empty_string_stream_id_panics_bug. Also makes encoding module items pub (from pub(crate)) with proper #[must_use] and error docs so integration tests can exercise them directly. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two bugs fixed:
- Empty stream_id caused panic in fjall's lsm-tree ("key may not be
empty"). Now returns FjallError::EmptyStreamId at the boundary.
- Numeric stream ID counter used fetch_add which wraps silently on
u64::MAX overflow, causing two different stream_id strings to share
the same key space (silent data corruption). Now uses checked
arithmetic and returns FjallError::StreamIdExhausted.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Introduce a StreamId newtype that enforces invariants at construction time: aggregate names must be ASCII alphanumeric + underscore, IDs must be non-empty, and the combined string must be null-free and within 512 bytes. A separate from_persisted constructor allows store adapters to load historical stream IDs with relaxed format checks. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
BREAKING CHANGE: RawEventStore, Repository, PendingEnvelope now use StreamId for compile-time stream ID validity. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
StreamId newtype makes empty stream IDs impossible at the type level. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
36 penetration tests across 18 attack categories: version arithmetic overflow proof, dual-instance corruption probe, deterministic simulation with shadow model, snapshot isolation, crash simulation via mem::forget, key boundary conditions, recovery torture (100 streams x 5 reopens), concurrent append storm (50 tasks), encoding boundary attacks, stream ID exhaustion, and more. Bugs documented (not fixed): 1. store.rs:109 — unchecked u64 addition `current_version + 1 + i_u64` overflows when current_version is near u64::MAX (panic in debug, silent wrap in release) 2. store.rs:61 — empty batch early return bypasses version validation, allowing append with wrong expected_version to succeed silently 3. schema_version=0 accepted on write but rejected on read (data written that can never be read back) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ursive asserts Bug 1 (CRITICAL): Version arithmetic overflow in sequential validation. current_version + 1 + i uses unchecked addition — panics in debug, wraps silently in release. Fixed with checked_add in both InMemoryStore and FjallStore. Bug 2 (MEDIUM): Empty batch bypasses version check in FjallStore. append(nonexistent, version_999, &[]) succeeded despite version mismatch. Fixed: version check now runs before empty-batch early return. Bug 3 (MEDIUM): schema_version=0 write-read asymmetry. PendingEnvelope builder accepted 0, but PersistedEnvelope::try_new rejects 0 on read — creating unreadable data. Fixed: builder clamps 0 to 1. Bug 4 (CRITICAL): Recursive debug_assert in encoding functions. encode_event_key called decode_event_key in a postcondition, which called encode_event_key — infinite mutual recursion causing stack overflow in all debug-mode tests. Removed all mutually-recursive postconditions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…trait, simplify API Major architectural refactoring across all crates: - Remove StreamId newtype from kernel; streams identified by generic Id - Replace TransformChain/SchemaTransform with single Upcaster trait - Add EventMorsel type for zero-copy event transformation - Add StreamLabel for type-safe stream identification in store layer - Add #[nexus::transforms] proc macro for compile-time upcaster generation - Simplify FjallStore (~400 lines removed), remove redundant validation - Replace exhaustive test suites (military/nasa_break_everything) with focused property tests, resilience tests, and wire format snapshots - Update all examples, compile-fail tests, and benchmarks for new API - Include .snap files in nix source filter for insta snapshot tests - Update CLAUDE.md with comprehensive coding rules from audit Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add Store<S> as Arc-wrapped shared handle to any RawEventStore - Store::repository(codec, upcaster) creates typed EventStore facades - Store::zero_copy_repository(codec, upcaster) for borrowing codecs - Split EventStore and ZeroCopyEventStore into separate modules - Reorganize nexus-store from flat files into codec/, envelope/, store/, and upcasting/ directories - Migrate all tests and examples to new Store::repository() API - Update CLAUDE.md architecture documentation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds `serde` and `json` features to nexus-store so users get ready-made codecs without hand-implementing `Codec<E>`: - `serde` feature: `SerdeFormat` trait + `SerdeCodec<F>` generic codec - `json` feature (implies serde): `Json` format + `JsonCodec` alias Users derive Serialize/Deserialize on their event enum and use `JsonCodec::default()` — zero boilerplate codec implementation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace `Store::repository(codec, upcaster)` and `Store::zero_copy_repository(codec, upcaster)` with a fluent builder: store.repository().codec(c).upcaster(u).build() store.repository().codec(c).build_zero_copy() With the `json` feature, JsonCodec is the default — zero-config usage: store.repository().build() NeedsCodec (!Send) prevents calling build() without a codec configured. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Major milestone: nexus goes from kernel-only to a full event sourcing stack with persistence.
codec/,envelope/,upcasting/,store/directories. IntroducedStore<S>Arc-wrapped shared handle. SeparatedRawEventStore(bytes) from typed facades (EventStore,ZeroCopyEventStore).SerdeCodec<F>withSerdeFormattrait abstraction.JsonCodecenabled via"json"feature. Zero-boilerplate event serialization.Store::repository(). Compile-time enforcement: can't build without codec (NeedsCodecis!Send). Auto-wiresJsonCodecwhen"json"feature is enabled.StreamId, migrated toUpcastertrait with compile-time validated#[nexus::transforms]macro,EventMorselzero-copy pipeline.Breaking Changes
StreamIdremoved — streams identified by anyDisplaytype viaStreamLabelStore::repository()/Store::zero_copy_repository()removed — usestore.repository().codec(c).upcaster(u).build()UpcasterChainremoved — use#[nexus::transforms]macro or implementUpcastertrait directlyPendingEnvelope/PersistedEnvelopenow in separate modulesStats
Test plan
🤖 Generated with Claude Code