const FNV1A_BASIS: u64 = 0xcbf29ce484222325;
const FNV1A_PRIME: u64 = 0x00000100000001B3;
pub fn fnv1a_64(data: &[u8]) -> u64 {
let mut hash = FNV1A_BASIS;
for &byte in data {
hash ^= byte as u64;
hash = hash.wrapping_mul(FNV1A_PRIME);
}
hash
}
pub use crate::canon::compute_event_digest;
pub fn hash_meshlet(positions: &[[f32; 3]], indices: &[u32]) -> u64 {
let mut hash = FNV1A_BASIS;
for pos in positions {
for &f in pos {
let bytes = f.to_le_bytes();
for &b in &bytes {
hash ^= b as u64;
hash = hash.wrapping_mul(FNV1A_PRIME);
}
}
}
for &idx in indices {
let bytes = idx.to_le_bytes();
for &b in &bytes {
hash ^= b as u64;
hash = hash.wrapping_mul(FNV1A_PRIME);
}
}
hash
}
pub fn canon_normalize(s: &str) -> String {
s.trim().to_lowercase()
}
pub fn build_scribe_id(
universe_id: &str,
timeline_id: &str,
world_id: &str,
region_id: &str,
epoch_id: &str,
shard_id: &str,
kind: &str,
topic: &str,
template_version: &str,
) -> String {
format!(
"scribe:{}:{}:{}:{}:{}:{}:{}:{}:{}",
canon_normalize(universe_id),
canon_normalize(timeline_id),
canon_normalize(world_id),
canon_normalize(region_id),
canon_normalize(epoch_id),
canon_normalize(shard_id),
canon_normalize(kind),
canon_normalize(topic),
canon_normalize(template_version),
)
}
pub fn build_codex_id(
universe_id: &str,
timeline_id: &str,
world_id: &str,
region_id: &str,
epoch_id: &str,
shard_id: &str,
query_key: &str,
params_json: &str,
) -> String {
let input = format!(
"scope={}:{}:{}:{}:{}:{}|qk={}|params={}|qtv=v1|sv=dreamwell.codex.v1",
canon_normalize(universe_id),
canon_normalize(timeline_id),
canon_normalize(world_id),
canon_normalize(region_id),
canon_normalize(epoch_id),
canon_normalize(shard_id),
canon_normalize(query_key),
params_json.trim(),
);
let hash = fnv1a_64(input.as_bytes());
format!("codex:{:016x}:v1", hash)
}
pub fn build_chronicle_id(
universe_id: &str,
timeline_id: &str,
world_id: &str,
region_id: &str,
epoch_id: &str,
shard_id: &str,
query_key: &str,
params_json: &str,
tick_min: u64,
tick_max: u64,
depth: u32,
format: &str,
) -> String {
let input = format!(
"scope={}:{}:{}:{}:{}:{}|qk={}|params={}|tmin={}|tmax={}|d={}|fmt={}|ctv=v1|sv=dreamwell.chronicle.v1",
canon_normalize(universe_id),
canon_normalize(timeline_id),
canon_normalize(world_id),
canon_normalize(region_id),
canon_normalize(epoch_id),
canon_normalize(shard_id),
canon_normalize(query_key),
params_json.trim(),
tick_min,
tick_max,
depth,
canon_normalize(format),
);
let hash = fnv1a_64(input.as_bytes());
format!("chronicle:{:016x}:v1", hash)
}
pub fn build_link_id(
universe_id: &str,
timeline_id: &str,
world_id: &str,
region_id: &str,
epoch_id: &str,
shard_id: &str,
from_kind: &str,
from_id: &str,
to_kind: &str,
to_id: &str,
reason: &str,
) -> String {
let input = format!(
"scope={}:{}:{}:{}:{}:{}|fk={}|fi={}|tk={}|ti={}|r={}",
canon_normalize(universe_id),
canon_normalize(timeline_id),
canon_normalize(world_id),
canon_normalize(region_id),
canon_normalize(epoch_id),
canon_normalize(shard_id),
canon_normalize(from_kind),
canon_normalize(from_id),
canon_normalize(to_kind),
canon_normalize(to_id),
canon_normalize(reason),
);
let hash = fnv1a_64(input.as_bytes());
format!("link:{:016x}", hash)
}
pub fn build_citation_log_id(artifact_kind: &str, artifact_id: &str, event_id: &str, tick: u64) -> String {
let input = format!("ak={}|ai={}|ei={}|t={}", artifact_kind, artifact_id, event_id, tick);
let hash = fnv1a_64(input.as_bytes());
format!("log:{:016x}", hash)
}
pub fn build_citation_str(event_id: &str, tick: u64) -> String {
format!("{}@{}", event_id, tick)
}
pub fn build_citation_key(event_id: &str, tick: u64) -> String {
let input = format!("evt={}|tick={}", event_id, tick);
let hash = fnv1a_64(input.as_bytes());
format!("{:016x}", hash)
}
pub const CITATION_ROLL_SEED: &str = "dreamwell.citations.v1";
pub fn citation_roll_seed_hash() -> String {
let hash = fnv1a_64(CITATION_ROLL_SEED.as_bytes());
format!("{:016x}", hash)
}
pub fn citation_roll_advance(prev_roll_hex: &str, citation_key_hex: &str, tick: u64) -> String {
let input = format!("{}|{}|{}", prev_roll_hex, citation_key_hex, tick);
let hash = fnv1a_64(input.as_bytes());
format!("{:016x}", hash)
}
pub const HOT_MAX_SCRIBE: usize = 16;
pub const HOT_MAX_CODEX: usize = 32;
pub const HOT_MAX_CHRONICLE: usize = 64;
pub const RECENT_KEYS_SCRIBE: usize = 64;
pub const RECENT_KEYS_CODEX: usize = 128;
pub const RECENT_KEYS_CHRONICLE: usize = 128;
#[derive(Debug, Clone)]
pub struct CitationBuffer {
pub hot: Vec<String>,
pub roll_hash: String,
pub total: u64,
pub min_tick: Option<u64>,
pub max_tick: u64,
pub recent_keys: Vec<String>,
}
impl CitationBuffer {
pub fn new() -> Self {
Self {
hot: Vec::new(),
roll_hash: citation_roll_seed_hash(),
total: 0,
min_tick: None,
max_tick: 0,
recent_keys: Vec::new(),
}
}
pub fn apply(&mut self, event_id: &str, tick: u64, hot_max: usize, recent_keys_max: usize) -> bool {
let recent_keys_max = recent_keys_max.max(1);
let cit_key = build_citation_key(event_id, tick);
if self.recent_keys.iter().any(|k| k == &cit_key) {
return false;
}
let cit_str = build_citation_str(event_id, tick);
self.hot.push(cit_str);
if self.hot.len() > hot_max {
let excess = self.hot.len() - hot_max;
self.hot.drain(0..excess);
}
self.roll_hash = citation_roll_advance(&self.roll_hash, &cit_key, tick);
self.total += 1;
match self.min_tick {
None => self.min_tick = Some(tick),
Some(current_min) if tick < current_min => self.min_tick = Some(tick),
_ => {}
}
if tick > self.max_tick {
self.max_tick = tick;
}
self.recent_keys.push(cit_key);
if self.recent_keys.len() > recent_keys_max {
let excess = self.recent_keys.len() - recent_keys_max;
self.recent_keys.drain(0..excess);
}
true
}
}
impl Default for CitationBuffer {
fn default() -> Self {
Self::new()
}
}
pub fn apply_bounded_citation(
citations_hot: &mut Vec<String>,
citations_roll_hash: &mut String,
citations_total: &mut u64,
citations_min_tick: &mut Option<u64>,
citations_max_tick: &mut u64,
citations_recent_keys: &mut Vec<String>,
event_id: &str,
tick: u64,
hot_max: usize,
recent_keys_max: usize,
) -> bool {
let mut buf = CitationBuffer {
hot: std::mem::take(citations_hot),
roll_hash: std::mem::take(citations_roll_hash),
total: *citations_total,
min_tick: *citations_min_tick,
max_tick: *citations_max_tick,
recent_keys: std::mem::take(citations_recent_keys),
};
let accepted = buf.apply(event_id, tick, hot_max, recent_keys_max);
*citations_hot = buf.hot;
*citations_roll_hash = buf.roll_hash;
*citations_total = buf.total;
*citations_min_tick = buf.min_tick;
*citations_max_tick = buf.max_tick;
*citations_recent_keys = buf.recent_keys;
accepted
}
pub fn build_scribe_index_id(
universe_id: &str,
timeline_id: &str,
world_id: &str,
region_id: &str,
epoch_id: &str,
shard_id: &str,
kind: &str,
topic: &str,
) -> String {
format!(
"idx::{}::{}::{}::{}::{}::{}::{}::{}",
canon_normalize(universe_id),
canon_normalize(timeline_id),
canon_normalize(world_id),
canon_normalize(region_id),
canon_normalize(epoch_id),
canon_normalize(shard_id),
canon_normalize(kind),
canon_normalize(topic),
)
}
pub fn build_stat_id(
universe_id: &str,
timeline_id: &str,
world_id: &str,
stats_scope: &str,
query_key: &str,
) -> String {
format!(
"stat::{}::{}::{}::{}::{}",
canon_normalize(universe_id),
canon_normalize(timeline_id),
canon_normalize(world_id),
canon_normalize(stats_scope),
canon_normalize(query_key),
)
}
pub fn checksum_content(json: &str) -> Result<String, String> {
let parsed: serde_json::Value =
serde_json::from_str(json).map_err(|e| format!("checksum_content_invalid_json: {}", e))?;
let canonical = serde_json::to_string(&parsed).map_err(|e| format!("checksum_content_serialize_failed: {}", e))?;
Ok(format!("{:016x}", fnv1a_64(canonical.as_bytes())))
}
pub fn build_alias_id(universe_id: &str, timeline_id: &str, world_id: &str, alias_key: &str) -> String {
format!(
"alias::{}::{}::{}::{}",
canon_normalize(universe_id),
canon_normalize(timeline_id),
canon_normalize(world_id),
canon_normalize(alias_key),
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fnv1a_64_deterministic_same_input() {
let a = fnv1a_64(b"hello world");
let b = fnv1a_64(b"hello world");
assert_eq!(a, b);
}
#[test]
fn fnv1a_64_different_inputs_differ() {
assert_ne!(fnv1a_64(b"alpha"), fnv1a_64(b"bravo"));
}
#[test]
fn fnv1a_64_empty_string() {
assert_eq!(fnv1a_64(b""), 0xcbf29ce484222325);
}
#[test]
fn fnv1a_64_single_byte_a() {
assert_eq!(fnv1a_64(b"a"), 0xaf63dc4c8601ec8c);
}
#[test]
fn fnv1a_64_foobar() {
assert_eq!(fnv1a_64(b"foobar"), 0x85944171f73967e8);
}
#[test]
fn fnv1a_64_single_null_byte() {
assert_eq!(fnv1a_64(&[0u8]), 0xaf63bd4c8601b7df);
}
#[test]
fn canon_normalize_trims_and_lowercases() {
assert_eq!(canon_normalize(" Hello WORLD "), "hello world");
}
#[test]
fn canon_normalize_no_op_on_clean_input() {
assert_eq!(canon_normalize("already_clean"), "already_clean");
}
#[test]
fn build_scribe_id_format() {
let id = build_scribe_id("U1", "TL1", "W1", "R1", "E1", "S1", "narrative", "topic1", "v2");
assert_eq!(id, "scribe:u1:tl1:w1:r1:e1:s1:narrative:topic1:v2");
}
#[test]
fn build_scribe_id_normalizes_whitespace_and_case() {
let a = build_scribe_id("U1", "TL1", "W1", "R1", "E1", "S1", "Narrative", "Topic1", "V2");
let b = build_scribe_id(
" u1 ",
" tl1 ",
" w1 ",
" r1 ",
" e1 ",
" s1 ",
" narrative ",
" topic1 ",
" v2 ",
);
assert_eq!(a, b);
}
#[test]
fn build_scribe_id_deterministic() {
let a = build_scribe_id("u", "t", "w", "r", "e", "s", "k", "p", "v1");
let b = build_scribe_id("u", "t", "w", "r", "e", "s", "k", "p", "v1");
assert_eq!(a, b);
}
#[test]
fn build_codex_id_format() {
let id = build_codex_id("u1", "tl1", "w1", "r1", "e1", "s1", "qk1", "{}");
assert!(id.starts_with("codex:"));
assert!(id.ends_with(":v1"));
assert_eq!(id.len(), 25);
}
#[test]
fn build_codex_id_deterministic() {
let a = build_codex_id("u", "t", "w", "r", "e", "s", "q", "{}");
let b = build_codex_id("u", "t", "w", "r", "e", "s", "q", "{}");
assert_eq!(a, b);
}
#[test]
fn build_codex_id_differs_on_query_key() {
let a = build_codex_id("u", "t", "w", "r", "e", "s", "query_a", "{}");
let b = build_codex_id("u", "t", "w", "r", "e", "s", "query_b", "{}");
assert_ne!(a, b);
}
#[test]
fn build_chronicle_id_format() {
let id = build_chronicle_id("u1", "tl1", "w1", "r1", "e1", "s1", "qk1", "{}", 0, 100, 3, "json");
assert!(id.starts_with("chronicle:"));
assert!(id.ends_with(":v1"));
}
#[test]
fn build_chronicle_id_deterministic() {
let a = build_chronicle_id("u", "t", "w", "r", "e", "s", "q", "{}", 1, 10, 2, "json");
let b = build_chronicle_id("u", "t", "w", "r", "e", "s", "q", "{}", 1, 10, 2, "json");
assert_eq!(a, b);
}
#[test]
fn build_chronicle_id_differs_on_tick_range() {
let a = build_chronicle_id("u", "t", "w", "r", "e", "s", "q", "{}", 0, 100, 1, "json");
let b = build_chronicle_id("u", "t", "w", "r", "e", "s", "q", "{}", 0, 200, 1, "json");
assert_ne!(a, b);
}
#[test]
fn build_link_id_format() {
let id = build_link_id("u1", "tl1", "w1", "r1", "e1", "s1", "actor", "a1", "item", "i1", "owns");
assert!(id.starts_with("link:"));
assert_eq!(id.len(), 21);
}
#[test]
fn build_link_id_deterministic() {
let a = build_link_id("u", "t", "w", "r", "e", "s", "fk", "fi", "tk", "ti", "reason");
let b = build_link_id("u", "t", "w", "r", "e", "s", "fk", "fi", "tk", "ti", "reason");
assert_eq!(a, b);
}
#[test]
fn build_link_id_differs_on_direction() {
let ab = build_link_id("u", "t", "w", "r", "e", "s", "actor", "a1", "item", "i1", "r");
let ba = build_link_id("u", "t", "w", "r", "e", "s", "item", "i1", "actor", "a1", "r");
assert_ne!(ab, ba);
}
#[test]
fn build_citation_log_id_format() {
let id = build_citation_log_id("scribe", "scribe_001", "evt:main:1:000000", 1);
assert!(id.starts_with("log:"));
assert_eq!(id.len(), 20);
}
#[test]
fn build_citation_log_id_deterministic() {
let a = build_citation_log_id("codex", "c1", "evt:m:1:000000", 42);
let b = build_citation_log_id("codex", "c1", "evt:m:1:000000", 42);
assert_eq!(a, b);
}
#[test]
fn build_citation_str_format() {
assert_eq!(build_citation_str("evt:main:1:000000", 42), "evt:main:1:000000@42");
}
#[test]
fn build_citation_key_is_hex() {
let key = build_citation_key("evt:main:1:000000", 1);
assert_eq!(key.len(), 16);
assert!(key.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn build_citation_key_deterministic() {
let a = build_citation_key("evt:m:1:000000", 5);
let b = build_citation_key("evt:m:1:000000", 5);
assert_eq!(a, b);
}
#[test]
fn citation_roll_seed_hash_is_hex() {
let h = citation_roll_seed_hash();
assert_eq!(h.len(), 16);
assert!(h.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn citation_roll_seed_hash_deterministic() {
assert_eq!(citation_roll_seed_hash(), citation_roll_seed_hash());
}
#[test]
fn citation_roll_advance_deterministic() {
let a = citation_roll_advance("abcdef0123456789", "1234567890abcdef", 10);
let b = citation_roll_advance("abcdef0123456789", "1234567890abcdef", 10);
assert_eq!(a, b);
}
#[test]
fn citation_roll_advance_changes_with_tick() {
let a = citation_roll_advance("abcdef0123456789", "1234567890abcdef", 10);
let b = citation_roll_advance("abcdef0123456789", "1234567890abcdef", 11);
assert_ne!(a, b);
}
#[test]
fn apply_bounded_citation_accepts_first() {
let mut hot = Vec::new();
let mut roll = citation_roll_seed_hash();
let mut total = 0u64;
let mut min_tick = None;
let mut max_tick = 0u64;
let mut recent = Vec::new();
let accepted = apply_bounded_citation(
&mut hot,
&mut roll,
&mut total,
&mut min_tick,
&mut max_tick,
&mut recent,
"evt:m:1:000000",
5,
HOT_MAX_SCRIBE,
RECENT_KEYS_SCRIBE,
);
assert!(accepted);
assert_eq!(total, 1);
assert_eq!(min_tick, Some(5));
assert_eq!(max_tick, 5);
assert_eq!(hot.len(), 1);
assert_eq!(recent.len(), 1);
}
#[test]
fn apply_bounded_citation_rejects_duplicate() {
let mut hot = Vec::new();
let mut roll = citation_roll_seed_hash();
let mut total = 0u64;
let mut min_tick = None;
let mut max_tick = 0u64;
let mut recent = Vec::new();
apply_bounded_citation(
&mut hot,
&mut roll,
&mut total,
&mut min_tick,
&mut max_tick,
&mut recent,
"evt:m:1:000000",
5,
HOT_MAX_SCRIBE,
RECENT_KEYS_SCRIBE,
);
let dup = apply_bounded_citation(
&mut hot,
&mut roll,
&mut total,
&mut min_tick,
&mut max_tick,
&mut recent,
"evt:m:1:000000",
5,
HOT_MAX_SCRIBE,
RECENT_KEYS_SCRIBE,
);
assert!(!dup);
assert_eq!(total, 1); }
#[test]
fn apply_bounded_citation_evicts_oldest_when_full() {
let mut hot = Vec::new();
let mut roll = citation_roll_seed_hash();
let mut total = 0u64;
let mut min_tick = None;
let mut max_tick = 0u64;
let mut recent = Vec::new();
let hot_max = 3;
let recent_max = 64;
for i in 0..5u64 {
let eid = format!("evt:m:{}:000000", i);
apply_bounded_citation(
&mut hot,
&mut roll,
&mut total,
&mut min_tick,
&mut max_tick,
&mut recent,
&eid,
i + 1,
hot_max,
recent_max,
);
}
assert_eq!(hot.len(), hot_max);
assert_eq!(total, 5);
assert!(hot[0].contains("evt:m:2:000000"));
assert!(hot[1].contains("evt:m:3:000000"));
assert!(hot[2].contains("evt:m:4:000000"));
}
#[test]
fn apply_bounded_citation_tracks_min_max_tick() {
let mut hot = Vec::new();
let mut roll = citation_roll_seed_hash();
let mut total = 0u64;
let mut min_tick = None;
let mut max_tick = 0u64;
let mut recent = Vec::new();
apply_bounded_citation(
&mut hot,
&mut roll,
&mut total,
&mut min_tick,
&mut max_tick,
&mut recent,
"evt:a",
10,
16,
64,
);
apply_bounded_citation(
&mut hot,
&mut roll,
&mut total,
&mut min_tick,
&mut max_tick,
&mut recent,
"evt:b",
3,
16,
64,
);
apply_bounded_citation(
&mut hot,
&mut roll,
&mut total,
&mut min_tick,
&mut max_tick,
&mut recent,
"evt:c",
20,
16,
64,
);
assert_eq!(min_tick, Some(3));
assert_eq!(max_tick, 20);
}
#[test]
fn build_scribe_index_id_format() {
let id = build_scribe_index_id("U1", "TL1", "W1", "R1", "E1", "S1", "narrative", "main_quest");
assert_eq!(id, "idx::u1::tl1::w1::r1::e1::s1::narrative::main_quest");
}
#[test]
fn build_scribe_index_id_deterministic() {
let a = build_scribe_index_id("u", "t", "w", "r", "e", "s", "k", "topic");
let b = build_scribe_index_id("u", "t", "w", "r", "e", "s", "k", "topic");
assert_eq!(a, b);
}
#[test]
fn build_stat_id_format() {
let id = build_stat_id("U1", "TL1", "W1", "region", "combat_queries");
assert_eq!(id, "stat::u1::tl1::w1::region::combat_queries");
}
#[test]
fn build_stat_id_deterministic() {
let a = build_stat_id("u", "t", "w", "scope", "qk");
let b = build_stat_id("u", "t", "w", "scope", "qk");
assert_eq!(a, b);
}
#[test]
fn checksum_content_deterministic() {
let a = checksum_content(r#"{"key":"value","num":42}"#).unwrap();
let b = checksum_content(r#"{"key":"value","num":42}"#).unwrap();
assert_eq!(a, b);
}
#[test]
fn checksum_content_is_16_hex() {
let c = checksum_content(r#"{"hello":"world"}"#).unwrap();
assert_eq!(c.len(), 16);
assert!(c.chars().all(|ch| ch.is_ascii_hexdigit()));
}
#[test]
fn checksum_content_normalizes_key_order() {
let a = checksum_content(r#"{"b":2,"a":1}"#).unwrap();
let b = checksum_content(r#"{"a":1,"b":2}"#).unwrap();
assert_eq!(a, b);
}
#[test]
fn checksum_content_differs_on_different_data() {
let a = checksum_content(r#"{"x":1}"#).unwrap();
let b = checksum_content(r#"{"x":2}"#).unwrap();
assert_ne!(a, b);
}
#[test]
fn checksum_content_invalid_json_returns_error() {
let result = checksum_content("not json {{{");
assert!(result.is_err());
assert!(result.unwrap_err().contains("checksum_content_invalid_json"));
}
#[test]
fn build_alias_id_format() {
let id = build_alias_id("U1", "TL1", "W1", "player.main_char");
assert_eq!(id, "alias::u1::tl1::w1::player.main_char");
}
#[test]
fn build_alias_id_deterministic() {
let a = build_alias_id("u", "t", "w", "key");
let b = build_alias_id("u", "t", "w", "key");
assert_eq!(a, b);
}
#[test]
fn hot_max_constants_are_nonzero() {
assert!(HOT_MAX_SCRIBE > 0);
assert!(HOT_MAX_CODEX > 0);
assert!(HOT_MAX_CHRONICLE > 0);
}
#[test]
fn recent_keys_constants_are_nonzero() {
assert!(RECENT_KEYS_SCRIBE > 0);
assert!(RECENT_KEYS_CODEX > 0);
assert!(RECENT_KEYS_CHRONICLE > 0);
}
#[test]
fn hot_max_less_than_or_equal_to_recent_keys() {
assert!(HOT_MAX_SCRIBE <= RECENT_KEYS_SCRIBE);
assert!(HOT_MAX_CODEX <= RECENT_KEYS_CODEX);
assert!(HOT_MAX_CHRONICLE <= RECENT_KEYS_CHRONICLE);
}
#[test]
fn citation_buffer_new_defaults() {
let buf = CitationBuffer::new();
assert!(buf.hot.is_empty());
assert_eq!(buf.total, 0);
assert_eq!(buf.min_tick, None);
assert_eq!(buf.max_tick, 0);
assert!(buf.recent_keys.is_empty());
}
#[test]
fn citation_buffer_accepts_first() {
let mut buf = CitationBuffer::new();
let accepted = buf.apply("evt:m:1:000000", 5, HOT_MAX_SCRIBE, RECENT_KEYS_SCRIBE);
assert!(accepted);
assert_eq!(buf.total, 1);
assert_eq!(buf.min_tick, Some(5));
assert_eq!(buf.max_tick, 5);
assert_eq!(buf.hot.len(), 1);
assert_eq!(buf.recent_keys.len(), 1);
}
#[test]
fn citation_buffer_rejects_duplicate() {
let mut buf = CitationBuffer::new();
buf.apply("evt:m:1:000000", 5, HOT_MAX_SCRIBE, RECENT_KEYS_SCRIBE);
let dup = buf.apply("evt:m:1:000000", 5, HOT_MAX_SCRIBE, RECENT_KEYS_SCRIBE);
assert!(!dup);
assert_eq!(buf.total, 1);
}
#[test]
fn citation_buffer_tracks_min_max_tick() {
let mut buf = CitationBuffer::new();
buf.apply("evt:a", 10, 16, 64);
buf.apply("evt:b", 3, 16, 64);
buf.apply("evt:c", 20, 16, 64);
assert_eq!(buf.min_tick, Some(3));
assert_eq!(buf.max_tick, 20);
}
#[test]
fn citation_buffer_min_tick_none_before_first() {
let buf = CitationBuffer::new();
assert_eq!(buf.min_tick, None);
}
}