Storage engine & key layout¶
Architecture
Normative spec
This page documents .ai/specs/core-engine/2026-07-02-composite-key-layout.md, Status: Draft, for peer review before PR-011 opens. It is the sole authority for every RocksDB key byte in Telha Core and the source for F1 (tenant isolation true-by-construction).
Every fact Telha stores, whether a node version, an edge, a vector, or a job, lives at a byte-exact address computed by one module. This page documents that module: the StorageEngine trait, the two backends that implement it, and the composite key layout that makes tenant isolation a property of construction rather than a property of discipline.
Overview & purpose¶
core-engine/src/storage/ is, in its own module doc, "the exclusive location for key encoding." Two problems motivate treating storage as its own layer rather than an implementation detail of the temporal engine:
- Tenant isolation must be true by construction, not by convention. Prefix bloom filters are a performance structure, not a security boundary. Isolation only holds if every read path builds keys through tenant-scoped code and every scan is bounded inside a tenant prefix. Ad-hoc key construction at call sites would make isolation unverifiable.
- "Latest version first" iteration needs a documented inversion rule. RocksDB's comparator sorts bytes ascending. Telha wants the newest version of an entity first. Those two facts are reconciled once, here, by inverting timestamps at encode time, rather than by every caller remembering to sort descending.
The result is a single crate-internal authority (KeyCodec, TenantScope, ScanRange) that every other subsystem (temporal, graph, query, vector, jobs) calls through, and a StorageEngine trait that the rest of the engine talks to without ever seeing a raw byte slice.
Design¶
The StorageEngine / StorageSnapshot abstraction¶
pub trait StorageSnapshot {
fn get(&self, cf: &str, key: &Key) -> Result<Option<Vec<u8>>, StorageError>;
fn scan_prefix(&self, cf: &str, range: &ScanRange) -> Result<KvIter<'_>, StorageError>;
}
pub trait StorageEngine: Send + Sync {
fn get(&self, cf: &str, key: &Key) -> Result<Option<Vec<u8>>, StorageError>;
fn put(&self, cf: &str, key: &Key, value: &[u8]) -> Result<(), StorageError>;
fn delete(&self, cf: &str, key: &Key) -> Result<(), StorageError>;
fn scan_prefix(&self, cf: &str, range: &ScanRange) -> Result<KvIter<'_>, StorageError>;
fn batch_write(&self, ops: Vec<WriteOp>) -> Result<(), StorageError>;
fn batch_write_durable(&self, ops: Vec<WriteOp>) -> Result<(), StorageError> { /* default: batch_write */ }
fn delete_range(&self, cf: &str, range: &ScanRange) -> Result<(), StorageError> { /* default: scan + batch delete */ }
fn snapshot(&self) -> Result<Box<dyn StorageSnapshot + '_>, StorageError>;
fn checkpoint(&self, dir: &std::path::Path) -> Result<(), StorageError>;
}
Two deliberate departures from a "generic KV store" shape, both recorded in core-engine/src/storage/engine.rs:
- Keys and scan bounds are typed, never raw bytes.
KeyandScanRangeare opaque outside the crate; the only way to build one is throughKeyCodecor aScanRangeconstructor, both of which take aTenantScopeas their first argument. This is the ratified deviation from the original PRD trait signature: no public surface anywhere in the engine accepts raw key bytes. batch_write_durableexists alongsidebatch_write. Most writes use the configuredstorage.sync_writespolicy; user-visible destruction events (tombstone cascades) must survive power loss the instant they are acknowledged, so they force a WAL sync regardless of that policy.
StorageSnapshot is deliberately not Send: RocksDB snapshots pin thread-local state, so a snapshot should be held briefly on one task and never carried across an await point that migrates threads.
Why MemoryEngine and a RocksDB backend¶
core-engine/src/storage/memory.rs implements StorageEngine over a BTreeMap per column family behind one RwLock. It exists as the semantic baseline: trivially correct ascending-byte-order scans, atomic batches by construction, and a shared conformance suite (core-engine/src/storage/conformance.rs) that both backends must pass unchanged. MemoryEngine is the fast backend for unit tests across the rest of the codebase; RocksDbEngine is the production backend. The same conformance suite is written to also gate the eventual Phase-4 FoundationDB backend (EPIC-401), so the acceptance bar for "is this a valid StorageEngine implementation" is one shared, executable test module rather than prose.
Why composite keys, not secondary indexes¶
The spec's Alternatives Considered section (§9) rejects the obvious alternatives explicitly:
| Alternative | Why it was rejected |
|---|---|
| Little-endian timestamps | Byte order would not equal numeric order; every scan would need a custom comparator. |
| String/hex UUIDs | 2.2x key size, breaks the fixed-width guarantee every constructor relies on. |
| Custom RocksDB comparator for descending time | Comparators complicate tooling, backups, and any future FoundationDB backend, which has no pluggable comparator. |
| Per-tenant column families or databases | Unbounded CF count harms compaction and memory; prefix isolation plus the F1 enforcement layers achieve the same guarantee for v1. |
| Hashed tenant prefix (siphash of tenant+org) | Destroys the ability to range-scan a tenant's data for export and GDPR erasure workflows. |
The chosen design instead pushes every access pattern into the key itself. node_history, valid_time_from, adjacency, tenant_all: each is a ScanRange constructor that derives a [prefix, successor(prefix)) bound directly from a TenantScope plus typed components. There is no secondary index to keep consistent with a primary store beyond the two temporal covering indexes described below, which store the primary key as their value specifically so that validating a hit is a point lookup, never a second scan.
flowchart TB
CALLERS["callers: temporal, graph, query, vector, jobs<br/>(typed requests only: TenantScope + ids + times)"]
CODEC["KeyCodec / ScanRange<br/>sole key authority"]
TRAIT["StorageEngine trait<br/>get / put / delete / scan_prefix / batch_write / snapshot"]
MEM["MemoryEngine<br/>BTreeMap per CF, one RwLock"]
ROCKS["RocksDbEngine<br/>prefix_extractor = fixed(32), bloom filters per CF"]
CFS["13 column families<br/>node_versions, edge_versions, adjacency_out/in,<br/>valid_time_index, tx_time_index, vectors, schema,<br/>sources, spans, traces, jobs, bitmap_index"]
CALLERS --> CODEC --> TRAIT
TRAIT --> MEM
TRAIT --> ROCKS
MEM --> CFS
ROCKS --> CFS Data model¶
Column families¶
core-engine/src/storage/mod.rs defines the CF name constants in pub mod cf and cf::ALL, the array the RocksDB backend opens in order. There are 13 column families as built (the spec's §4 diagram and RocksDB module doc comment describe "12"; bitmap_index was added afterward by the bitmap-predicate-index spec and is the 13th, tracked below in As-built notes).
| CF constant | CF name | Purpose |
|---|---|---|
cf::NODE_VERSIONS | node_versions | Node version records: the primary store of tritemporal node facts. |
cf::EDGE_VERSIONS | edge_versions | Edge version records: the primary store of tritemporal relationship facts. |
cf::ADJACENCY_OUT | adjacency_out | Outgoing adjacency entries, keyed by source node, for forward traversal. |
cf::ADJACENCY_IN | adjacency_in | Incoming adjacency entries, keyed by destination node, for reverse traversal. |
cf::VALID_TIME_INDEX | valid_time_index | Covering index leading on valid time, for global "what was true at T" scans. |
cf::TX_TIME_INDEX | tx_time_index | Covering index leading on transaction time, for global "what did we believe at T" scans. |
cf::SPANS | spans | Token span ledger: byte offsets into canonical source text, for provenance. |
cf::SOURCES | sources | Source document identity and version records; also hosts two reserved GDPR row shapes. |
cf::VECTORS | vectors | Embedding vectors, source of truth (HNSW partitions are derived, per the F4 doctrine). |
cf::SCHEMA | schema | Inferred schema versions per label; also hosts the system-scoped embedding-model registry. |
cf::TRACES | traces | Query and generation trace records. |
cf::JOBS | jobs | Internal job queue, tenant-scoped and system-scoped. |
cf::BITMAP_INDEX | bitmap_index | Roaring-bitmap predicate index: derived data, rebuildable via telha index rebuild-bitmaps. |
Primitive encodings¶
All key primitives are normative (spec §5.1) and fixed-width. There are no variable-length components anywhere in v1; a future one would need to be length-prefixed and the spec amended first.
| Primitive | Encoding | Why |
|---|---|---|
tenant_id, organization_id, logical ids | UUIDv7, 16 raw bytes | v7 time-ordering improves write locality. |
Timestamps (Ts = u64) | Microseconds since Unix epoch, big-endian | Big-endian makes byte order equal numeric order. |
| Inverted timestamps | u64::MAX - ts, big-endian, via inv() | Descending sort under RocksDB's ascending comparator gives latest-first iteration. |
EdgeTypeHash | xxh3_64 of the UTF-8 type string, big-endian, 8 bytes | Fixed width; a collision registry is planned in the schema CF (PR-017), fail-loud. |
LabelHash | xxh3_64 of the label string, big-endian, 8 bytes | Same rationale, used by schema and bitmap_index. |
Every tenant-scoped key begins with the 32-byte prefix produced by TenantScope::prefix(): [tenant_id (16)][organization_id (16)].
The reserved system namespace
The all-zero prefix [0u8; 32] is a ratified reserved scope (OQ-3, resolved 2026-07-02), used for deployment-global rows: system jobs and the embedding-model registry. System rows sort deterministically to the absolute head of each CF, which makes a bounded scan from the zero prefix a fast way to poll them. The scope is unforgeable externally: only the crate-internal SystemScope type can produce it, and TenantScope::new rejects nil components, so no external request path (REST auth, gRPC metadata, MCP session) can ever resolve to it.
Byte layouts per column family¶
Every layout below is read directly from core-engine/src/storage/keys.rs (the key_len constants and the KeyCodec encode functions), cross-checked against spec §5.2.
node_versions and edge_versions (identical shape, key_len::NODE_VERSION / EDGE_VERSION, 64 bytes):
[ tenant_id 16 ][ organization_id 16 ][ logical_id 16 ][ inv(valid_time_start) 8 ][ inv(tx_time_start) 8 ]
|<---------------- scope 32 ------------------------>|
total: 64 bytes
adjacency_out and adjacency_in (identical shape, key_len::ADJACENCY, 88 bytes). For adjacency_out, anchor is the source node and neighbor the destination; reversed for adjacency_in:
[ scope 32 ][ anchor_id 16 ][ edge_type_hash 8 ][ inv(valid_time_start) 8 ][ inv(tx_time_start) 8 ][ neighbor_id 16 ]
total: 88 bytes
valid_time_index and tx_time_index (identical shape, key_len::TEMPORAL_INDEX, 65 bytes). The CF name tells you which time dimension leads; valid_time_index stores (valid_time_start, tx_time_start) in that order, tx_time_index stores (tx_time_start, valid_time_start):
[ scope 32 ][ inv(primary_time) 8 ][ inv(secondary_time) 8 ][ logical_id 16 ][ kind 1 ]
total: 65 bytes
kind is IndexKind::Node = 0x00 or IndexKind::Edge = 0x01, letting one index serve both version CFs. The stored value at each index row is the primary CF key bytes (a covering index), so validating a candidate is a point lookup, never a second scan.
vectors (key_len::VECTOR, 72 bytes):
[ scope 32 ][ node_logical_id 16 ][ embedding_model_id 8 ][ inv(valid_time_start) 8 ][ inv(tx_time_start) 8 ]
total: 72 bytes
schema (key_len::SCHEMA, 48 bytes):
The same 48-byte shape hosts a second row family under the reserved system scope: KeyCodec::vector_model_key writes [nil 32][model_id 8][inv(tx_start) 8], the deployment-global embedding-model registry (PR-038). It is unreachable from any tenant scan because the nil prefix only exists via SystemScope.
sources (key_len::SOURCE, 56 bytes):
Two reserved row shapes reuse this exact 56-byte layout at the sentinel chunk index REDACTION_CHUNK_INDEX = u64::MAX (inv(u64::MAX) == 0, so these rows sort first in their family; real chunk indexes are dense from 0 and can never collide):
- Blob redaction record,
KeyCodec::redaction_record_key(scope, blob_id): marks a blob's content address as addressing-only. - Blocklist row,
KeyCodec::blocklist_key(scope, keyed_digest):source_idhere is not a real source id but a UUID derived fromblake3("telha/blocklist/v1" ‖ keyed_digest)[..16], namespaced so it can never collide with a real blob family (blob ids derive from raw canonical hashes).
spans (key_len::SPAN, 64 bytes). Offsets are non-inverted: spans iterate in document order, not latest-first:
traces (key_len::TRACE, 48 bytes):
jobs (key_len::JOB, 58 bytes). created_at is non-inverted, deliberately: FIFO within (state, priority) is wanted, not latest-first:
Tenant-scoped jobs use KeyCodec::job_key; system-scoped jobs (queue-wide maintenance, not tied to any tenant) use the crate-internal KeyCodec::system_job_key under SystemScope, producing the identical 58-byte shape with a nil 32-byte prefix.
bitmap_index is not in the original spec (it postdates it; see As-built notes) but is fully keyed through the same KeyCodec/TenantScope discipline. It is the one CF with more than one key shape, discriminated by a kind byte (core-engine/src/storage/keys.rs, bitmap_kind module):
common prefix: [ scope 32 ][ label_hash 8 ][ kind 1 ]
kind = 0x00 ALLOC [ ...prefix... ] total: 41 bytes
kind = 0x01 ID_MAP [ ...prefix... ][ row_id 4 ] total: 45 bytes
kind = 0x02 REV_MAP [ ...prefix... ][ logical_id 16 ] total: 57 bytes
kind = 0x03 POSTING [ ...prefix... ][ property_hash 8 ][ bucket_hash 8 ] total: 57 bytes
kind = 0x04 PROP_COUNTER [ ...prefix... ][ property_hash 8 ] total: 49 bytes
ALLOC: the monotonic row-id watermark for one(tenant, label).ID_MAP:row_id -> logical_id, keyed by the 4-byte big-endianrow_id.REV_MAP:logical_id -> row_id, the inverse map.POSTING: a roaring-bitmap posting list for one(property, value-bucket)pair, wherevalue-bucketis either an equality bucket or one of 16 order-preserving range bands.PROP_COUNTER: a per-property distinct-value counter, or the overflow markeru32::MAXonce the cap is hit.
Length reference¶
| Column family | Key length (bytes) |
|---|---|
node_versions | 64 |
edge_versions | 64 |
adjacency_out / adjacency_in | 88 |
valid_time_index / tx_time_index | 65 |
vectors | 72 |
schema | 48 |
sources | 56 |
spans | 64 |
traces | 48 |
jobs | 58 |
bitmap_index (ALLOC) | 41 |
bitmap_index (ID_MAP) | 45 |
bitmap_index (REV_MAP) | 57 |
bitmap_index (POSTING) | 57 |
bitmap_index (PROP_COUNTER) | 49 |
Key codec details¶
TenantScope, SystemScope, and nil-UUID rejection¶
pub struct TenantScope { tenant_id: Uuid, organization_id: Uuid } // fields private
impl TenantScope {
pub fn new(tenant_id: Uuid, organization_id: Uuid) -> Result<Self, KeyError> {
if tenant_id.is_nil() || organization_id.is_nil() {
return Err(KeyError::NilScope);
}
Ok(Self { tenant_id, organization_id })
}
}
pub struct SystemScope(pub(crate) ()); // the ONLY producer of [0u8; 32]
TenantScope::new is the only way to build a scope, and it rejects nil components with KeyError::NilScope. As-built, the fields are private with accessors (tenant_id(), organization_id()), which is a deliberate divergence from the spec's §6 sketch of a struct with public fields: public fields would allow literal construction that bypasses the nil check, defeating the unforgeability guarantee the reserved system namespace depends on. SystemScope is pub(crate)-constructible only (via SystemScope::get), so no external request path (REST auth, gRPC metadata, MCP session) can ever produce the all-zero prefix; the only way into the system namespace is code inside the storage module itself.
A third type, KeyScope, exists purely for decoded output: it carries the tenant and organization UUIDs read out of a key (which may legitimately be nil for system keys) for forensics and validation, and cannot be converted back into a TenantScope or used to build a scan.
ScanRange constructors¶
ScanRange holds private start/end byte vectors and is the only type StorageEngine::scan_prefix accepts. Every public constructor takes a TenantScope and derives both bounds from it, so no caller outside the storage module can construct a range that is not tenant-bounded. As read from core-engine/src/storage/keys.rs, the constructors are:
| Constructor | Access pattern |
|---|---|
ScanRange::node_history(scope, id) | All versions of one node, latest first. |
ScanRange::edge_history(scope, id) | All versions of one edge, latest first. |
ScanRange::node_history_after(scope, id, after_valid_start, after_tx_start) | Resume an entity-history scan strictly after a previously seen version (pagination cursor). |
ScanRange::valid_time_from(scope, t) | Global "valid at T" scan over valid_time_index, newest first. |
ScanRange::tx_time_from(scope, t) | Global "recorded at T" scan over tx_time_index, same shape. |
ScanRange::adjacency(scope, anchor, edge_type) | Adjacency scan for one anchor node, optionally narrowed to one edge type. |
ScanRange::vectors_for_node(scope, node_id, model) | Embedding history for one node, optionally narrowed to one model. |
ScanRange::schema_history(scope, label) | Schema version history for one label, latest first. |
ScanRange::source_history(scope, source_id) | Version history of one source document, latest first. |
ScanRange::spans_for_source(scope, source_id) | All spans of one source document, in document order (same key shape as source_history, different CF). |
ScanRange::jobs_in_state(scope, state) | Ready-to-lease jobs for one tenant in (state, priority) order. |
ScanRange::tenant_all(scope) | Every key of one tenant in one CF: export and GDPR-erasure walks. |
ScanRange::bitmap_label_all(scope, label) | All bitmap_index rows of one (tenant, label): rebuild sweeps and consistency checks. |
A handful of constructors are pub(crate) because they either touch the system namespace or exist for internal replay/maintenance paths, not for general callers: system_vector_models, system_vector_model_history, system_jobs_in_state, temporal_index_after, tenant_all_after_entity, from_stored_bounds, prefix_successor, and system_full_cf. These are discussed under Algorithms & invariants below, since each one is an explicit, documented exception to the tenant-confinement rule rather than an oversight.
Algorithms & invariants¶
The 32-byte prefix extractor and bloom filters¶
core-engine/src/storage/rocks.rs configures every column family identically at open time:
fn cf_options(name: &str, cache: &Cache) -> Options {
let mut block = BlockBasedOptions::default();
block.set_block_cache(cache);
block.set_bloom_filter(10.0, false);
block.set_whole_key_filtering(POINT_LOOKUP_CFS.contains(&name));
let mut opts = Options::default();
opts.set_block_based_table_factory(&block);
opts.set_prefix_extractor(rocksdb::SliceTransform::create_fixed_prefix(SCOPE_LEN)); // 32
opts.set_memtable_prefix_bloom_ratio(0.1);
opts
}
SCOPE_LEN is 32: the fixed prefix extractor is exactly the [tenant_id][organization_id] scope, which every key in every CF begins with, tenant-scoped or system-scoped. POINT_LOOKUP_CFS names the three CFs answered mostly by point lookups (node_versions, edge_versions, vectors), which additionally get whole-key bloom filtering; every other CF relies on the prefix bloom alone, since it is scan-heavy (adjacency, the two temporal indexes, bitmap_index, and so on).
Successor and range-safety¶
fn successor(bytes: &[u8]) -> Option<Vec<u8>> {
let mut out = bytes.to_vec();
for i in (0..out.len()).rev() {
if out[i] != 0xFF {
out[i] += 1;
out.truncate(i + 1);
return Some(out);
}
}
None
}
successor() returns the smallest byte string strictly greater than every string starting with bytes, by incrementing the last non-0xFF byte and truncating. ScanRange::within_scope uses it to build the upper bound of every scoped range, and falls back to the successor of the scope itself (not the finer prefix) when the finer prefix's suffix is all 0xFF, so a range's end can never overflow past the tenant's own boundary.
Every ScanRange must stay inside its tenant prefix
This is invariant 4 from spec §5.3, proptested directly in core-engine/src/storage/keys.rs (scan_ranges_stay_inside_tenant_prefix, 2048 generated cases per CI run): every ScanRange satisfies start.starts_with(scope_prefix) and end <= successor(scope_prefix). No range may cross the 32-byte prefix boundary. tenant_all, node_history, adjacency, and every other public constructor are covered.
Ordering invariants tested by proptest
Two more invariants from spec §5.3 are proptested directly: for a fixed (scope, logical_id), a later valid_time_start sorts strictly earlier (latest-first), and on a tie, a later tx_time_start sorts earlier still. Both fall out of the inv() inversion applied uniformly at encode time. Decode is also proptested to be injective (decode(encode(x)) == x), and every decoder is proptested and cargo-fuzzed to never panic on arbitrary bytes, only to return KeyError::{WrongLength, BadUuid, UnknownKind}.
Documented exceptions to tenant confinement¶
A small set of pub(crate) constructors deliberately step outside the per-tenant scan model, each recorded in the spec's changelog as a "system-maintenance exception" rather than a silent gap:
ScanRange::system_full_cf(): the entire key space of one CF, used only by the job-queue reaper to reclaim expired leases across every tenant without a tenant registry. Never derived from external input.ScanRange::from_stored_bounds(start, end): rehydrates a range from bounds this crate previously emitted into a GDPR-erasure replay program; the replay executor independently re-verifies tenant confinement before use.ScanRange::tenant_all_after_entity(scope, id)andScanRange::temporal_index_after(scope, cursor_key): pagination cursor helpers that stay inside one tenant's prefix by construction, rejecting a cursor whose prefix does not match the caller's scope.
Nil-UUID rejection as a security boundary¶
TenantScope::new rejecting nil UUIDs is not just input validation, it is the mechanism that makes the reserved system namespace unforgeable. Because UUIDv7 values are never nil, and tenant provisioning independently rejects nil UUIDs, a real tenant can never collide with the all-zero prefix, and because TenantScope::new itself rejects nil, no code path that authenticates an external caller into a TenantScope can ever produce it either.
Configuration¶
| Key | Default | Where |
|---|---|---|
storage.block_cache_mb | 256 | TelhaConfig::default(), core-engine/src/config.rs. Shared LRU block cache size across every CF. |
storage.sync_writes | true | Same file. Whether batch_write fsyncs the WAL; batch_write_durable always syncs regardless. |
| Prefix extractor | Fixed 32 bytes (SCOPE_LEN) | core-engine/src/storage/rocks.rs, applied to every CF. Not runtime-configurable. |
| Memtable prefix bloom ratio | 0.1 | set_memtable_prefix_bloom_ratio(0.1), every CF. |
| Block-based bloom filter | 10 bits per key | block.set_bloom_filter(10.0, false), every CF. |
| Whole-key filtering | On for node_versions, edge_versions, vectors; off elsewhere | POINT_LOOKUP_CFS in rocks.rs. |
Per the spec (§8), no key-layout element is runtime-configurable. Layout changes require a new spec and a migration plan, because stored keys carry no version byte (stored values do, landing with PR-013): a future key change is a rewrite migration by design, a cost the spec accepts deliberately since key stability underlies every index.
As-built notes¶
From spec §14 (Changelog), reconciled against the code actually in core-engine/src/storage/:
bitmap_indexis a 13th CF added after this spec's v0.1/v0.2/v0.3 entries. The spec's own architecture diagram (§4) and the RocksDB module's doc comment both still say "12 standard column families," butcf::ALLincore-engine/src/storage/mod.rslists 13, andcore-engine/src/storage/bitmap.rsimplements a full key family for it under the bitmap-predicate-index spec. Treat "12" in this spec's prose as stale; the code andcf::ALLare the current count.- v0.2 (2026-07-02): the nil-tenant system namespace was adopted, resolving OQ-3.
SystemScopeand the unforgeability guards (TenantScope::newnil rejection,pub(crate)-only construction) are present inkeys.rsexactly as described. - v0.3 (2026-07-03): a system-maintenance exception to invariant 4 added
ScanRange::system_full_cf,pub(crate), for the job-queue reaper. Present in the code as documented, gated to crate-internal use. - v0.4 (2026-07-03): the embedding-model registry reuses the 48-byte
schemaCF shape under the system prefix.KeyCodec::vector_model_key,ScanRange::system_vector_models, andScanRange::system_vector_model_historyall exist inkeys.rsexactly as the changelog describes, allpub(crate). - v0.5 (2026-07-06): the GDPR reserved sources-CF rows (§3.7 addendum) reuse the 56-byte
sourcesshape atREDACTION_CHUNK_INDEX = u64::MAX.KeyCodec::redaction_record_keyandKeyCodec::blocklist_keyboth exist inkeys.rsexactly as described, including theblake3-derived, namespaced blocklist id. The correspondingScanRange::tenant_allreuse andScanRange::from_stored_boundscrate-internal rehydration are present as well. - Not directly verifiable in the storage code alone: the spec's success metrics (§12), namely "100% of iterator/scan call sites pass the CI grep-gate," the "3-tenant dataset, zero cross-prefix rows" integration test, and the 24-hour fuzz run, are process and CI claims. The proptest suite that backs the ordering and confinement invariants is present and readable in
core-engine/src/storage/keys.rs, but the CI grep-gate itself and the fuzz target live outsidecore-engine/src/storage/and were not inspected for this page. - The spec's §6 API sketch shows
TenantScopewith public fields; the as-built type has private fields with accessors, a deviation the module doc inkeys.rscalls out directly (public fields would allow bypassing the nil check). describe_bitmap_keyincore-engine/src/storage/keys.rsand thetelha debug decode-keyforensics path described in spec §7 both exist, dispatched fromdescribe_keyincore-engine/src/storage/mod.rsby CF name for every CF includingbitmap_index.