1use kcode_speaker_types::{FeatureVector, Key, LabeledSample, ObjectId, RecordingKind, SegmentRef};
2use rusqlite::{Connection, params};
3use serde::{Deserialize, Serialize};
4use sha2::{Digest, Sha256};
5use std::{
6 collections::{BTreeMap, BTreeSet},
7 error::Error,
8 fmt,
9 path::Path,
10 sync::Mutex,
11};
12
13#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
14pub struct EventEnvelope {
15 pub event_id: Key,
16 pub event: Event,
17}
18
19#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
20pub enum Event {
21 RegisterSegment(SegmentRegistration),
22 RecordAttempt(AttemptRecord),
23 SelectAttempt(AttemptSelection),
24 SetSampleState(SampleStateChange),
25 PutArtifact(ArtifactRecord),
26 SetActiveModel(ActiveModelChange),
27}
28
29#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
30pub struct SegmentRegistration {
31 pub segment: SegmentRef,
32 pub group_id: Key,
33 pub recording_kind: RecordingKind,
34}
35
36#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
37pub struct AttemptRecord {
38 pub attempt_id: Key,
39 pub clip_object: ObjectId,
40 pub extraction_key: Key,
41 pub provider_result_object: Option<ObjectId>,
42 pub outcome: StoredAttemptOutcome,
43}
44
45#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
46pub enum StoredAttemptOutcome {
47 Scored {
48 recording_quality: u8,
49 speakers: Vec<StoredSpeaker>,
50 },
51 Unscorable {
52 reason: String,
53 },
54 InvalidResponse {
55 error: String,
56 },
57 ProviderFailure {
58 code: Key,
59 detail: String,
60 },
61}
62
63#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
64pub struct StoredSpeaker {
65 pub speaker_ordinal: u16,
66 pub primary_language: Key,
67 pub closest_dialect: String,
68 pub usable_speech_ms: u32,
69 pub features: FeatureVector,
70}
71
72#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
73pub struct AttemptSelection {
74 pub clip_object: ObjectId,
75 pub extraction_key: Key,
76 pub attempt_id: Key,
77 pub reason: String,
78}
79
80#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
81pub struct SampleStateChange {
82 pub sample_id: Key,
83 pub state: SampleState,
84 pub reason: String,
85}
86
87#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
88pub enum SampleState {
89 Unlabeled,
90 Confirmed { speaker_id: Key },
91 Retracted,
92}
93
94#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
95pub struct ArtifactRecord {
96 pub artifact_id: Key,
97 pub cohort_id: Key,
98 pub kind: ArtifactKind,
99 pub sha256: [u8; 32],
100 pub bytes: Vec<u8>,
101}
102
103#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
104pub enum ArtifactKind {
105 Model,
106 Evaluation,
107 ReplayManifest,
108}
109
110#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
111pub struct ActiveModelChange {
112 pub cohort_id: Key,
113 pub model_artifact_id: Option<Key>,
114 pub reason: String,
115}
116
117#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
118pub enum Request {
119 Commit(Vec<EventEnvelope>),
120 Read(Query),
121}
122
123#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
124pub enum Query {
125 Attempt {
126 attempt_id: Key,
127 },
128 Attempts {
129 extraction_key: Option<Key>,
130 source_object: Option<ObjectId>,
131 },
132 TrainingRows {
133 cohort_id: Key,
134 },
135 RepeatabilityRows {
136 cohort_id: Key,
137 },
138 Artifact {
139 artifact_id: Key,
140 },
141 ActiveModel {
142 cohort_id: Key,
143 },
144 Inventory {
145 extraction_key: Option<Key>,
146 },
147}
148
149#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
150pub struct Response {
151 pub revision: u64,
152 pub result: ResponseKind,
153}
154
155#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
156pub enum ResponseKind {
157 Commit { applied: u64 },
158 Attempt(Option<AttemptSnapshot>),
159 Attempts(Vec<AttemptSnapshot>),
160 TrainingRows(Vec<LabeledSample>),
161 RepeatabilityRows(Vec<LabeledSample>),
162 Artifact(Option<ArtifactRecord>),
163 ActiveModel(Option<Key>),
164 Inventory(InventorySnapshot),
165}
166
167#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
168pub struct AttemptSnapshot {
169 pub attempt: AttemptRecord,
170 pub sample_ids: Vec<Key>,
171 pub selected: bool,
172}
173
174#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
175pub struct SelectionSnapshot {
176 pub selection: AttemptSelection,
177}
178
179#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
180pub struct SampleStateSnapshot {
181 pub change: SampleStateChange,
182}
183
184#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
185pub struct ArtifactMetadata {
186 pub artifact_id: Key,
187 pub cohort_id: Key,
188 pub kind: ArtifactKind,
189 pub sha256: [u8; 32],
190 pub byte_len: u64,
191}
192
193#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
194pub struct ActiveModelSnapshot {
195 pub change: ActiveModelChange,
196}
197
198#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
199pub struct MissingSegmentOrdinals {
200 pub source_object: ObjectId,
201 pub ordinals: Vec<u16>,
202}
203
204#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
205pub struct OutcomeCounts {
206 pub scored: u64,
207 pub unscorable: u64,
208 pub invalid_response: u64,
209 pub provider_failure: u64,
210}
211
212#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
213pub struct InventorySnapshot {
214 pub registrations: Vec<SegmentRegistration>,
215 pub attempts: Vec<AttemptSnapshot>,
216 pub selections: Vec<SelectionSnapshot>,
217 pub sample_states: Vec<SampleStateSnapshot>,
218 pub artifacts: Vec<ArtifactMetadata>,
219 pub active_models: Vec<ActiveModelSnapshot>,
220 pub missing_segment_ordinals: Vec<MissingSegmentOrdinals>,
221 pub outcome_counts: OutcomeCounts,
222}
223
224#[derive(Debug)]
225pub enum StoreError {
226 Validation(String),
227 Conflict(String),
228 SchemaVersion(i64),
229 Storage(String),
230}
231
232impl fmt::Display for StoreError {
233 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
234 match self {
235 Self::Validation(v) => write!(f, "validation: {v}"),
236 Self::Conflict(v) => write!(f, "conflict: {v}"),
237 Self::SchemaVersion(v) => write!(f, "unsupported schema version {v}"),
238 Self::Storage(v) => write!(f, "storage: {v}"),
239 }
240 }
241}
242
243impl Error for StoreError {}
244
245impl From<rusqlite::Error> for StoreError {
246 fn from(value: rusqlite::Error) -> Self {
247 Self::Storage(value.to_string())
248 }
249}
250
251impl From<serde_json::Error> for StoreError {
252 fn from(value: serde_json::Error) -> Self {
253 Self::Storage(value.to_string())
254 }
255}
256
257pub struct Store {
258 connection: Mutex<Connection>,
259}
260
261pub fn open(path: impl AsRef<Path>) -> Result<Store, StoreError> {
262 let connection = Connection::open(path)?;
263 let version: i64 = connection.query_row("PRAGMA user_version", [], |row| row.get(0))?;
264 if version == 0 {
265 let objects: i64 = connection.query_row(
266 "SELECT count(*) FROM sqlite_schema WHERE name NOT LIKE 'sqlite_%'",
267 [],
268 |row| row.get(0),
269 )?;
270 if objects != 0 {
271 return Err(StoreError::SchemaVersion(0));
272 }
273 connection.execute_batch(
274 "CREATE TABLE events(
275 event_id TEXT PRIMARY KEY NOT NULL,
276 event_json BLOB NOT NULL
277 );
278 PRAGMA user_version = 1;",
279 )?;
280 } else if version != 1 {
281 return Err(StoreError::SchemaVersion(version));
282 }
283 connection.prepare("SELECT event_id,event_json FROM events LIMIT 0")?;
284 Ok(Store {
285 connection: Mutex::new(connection),
286 })
287}
288
289impl Store {
290 pub fn execute(&self, request: Request) -> Result<Response, StoreError> {
291 let mut connection = self
292 .connection
293 .lock()
294 .map_err(|_| StoreError::Storage("connection lock poisoned".into()))?;
295 let (mut event_ids, mut projection) = load(&connection)?;
296 match request {
297 Request::Read(query) => Ok(Response {
298 revision: event_ids.len() as u64,
299 result: projection.read(query)?,
300 }),
301 Request::Commit(events) => {
302 let mut additions = Vec::new();
303 for envelope in events {
304 let id = envelope.event_id.as_ref().to_owned();
305 let json = serde_json::to_vec(&envelope.event)?;
306 if let Some(old) = event_ids.get(&id) {
307 if old != &json {
308 return Err(StoreError::Conflict(format!(
309 "event ID {id} has different content"
310 )));
311 }
312 continue;
313 }
314 projection.apply(&envelope.event)?;
315 event_ids.insert(id.clone(), json.clone());
316 additions.push((id, json));
317 }
318 let transaction = connection.transaction()?;
319 for (id, json) in &additions {
320 transaction.execute(
321 "INSERT INTO events(event_id,event_json) VALUES(?1,?2)",
322 params![id, json],
323 )?;
324 }
325 transaction.commit()?;
326 Ok(Response {
327 revision: event_ids.len() as u64,
328 result: ResponseKind::Commit {
329 applied: additions.len() as u64,
330 },
331 })
332 }
333 }
334 }
335}
336
337#[derive(Default)]
338struct Projection {
339 registrations: BTreeMap<ObjectId, SegmentRegistration>,
340 attempts: BTreeMap<Key, AttemptRecord>,
341 selections: BTreeMap<(ObjectId, Key), AttemptSelection>,
342 states: BTreeMap<Key, SampleStateChange>,
343 artifacts: BTreeMap<Key, ArtifactRecord>,
344 active_models: BTreeMap<Key, ActiveModelChange>,
345}
346
347fn load(connection: &Connection) -> Result<(BTreeMap<String, Vec<u8>>, Projection), StoreError> {
348 let mut statement =
349 connection.prepare("SELECT event_id,event_json FROM events ORDER BY rowid")?;
350 let rows = statement.query_map([], |row| {
351 Ok((row.get::<_, String>(0)?, row.get::<_, Vec<u8>>(1)?))
352 })?;
353 let mut ids = BTreeMap::new();
354 let mut projection = Projection::default();
355 for row in rows {
356 let (id, json) = row?;
357 Key::parse(&id)
358 .map_err(|e| StoreError::Storage(format!("invalid stored event ID: {e}")))?;
359 let event: Event = serde_json::from_slice(&json)?;
360 projection.apply(&event)?;
361 ids.insert(id, json);
362 }
363 Ok((ids, projection))
364}
365
366impl Projection {
367 fn apply(&mut self, event: &Event) -> Result<(), StoreError> {
368 match event {
369 Event::RegisterSegment(registration) => self.register(registration),
370 Event::RecordAttempt(attempt) => self.record_attempt(attempt),
371 Event::SelectAttempt(selection) => self.select(selection),
372 Event::SetSampleState(change) => self.set_sample_state(change),
373 Event::PutArtifact(artifact) => self.put_artifact(artifact),
374 Event::SetActiveModel(change) => self.set_active_model(change),
375 }
376 }
377
378 fn register(&mut self, registration: &SegmentRegistration) -> Result<(), StoreError> {
379 registration
380 .segment
381 .validate()
382 .map_err(|e| StoreError::Validation(e.to_string()))?;
383 let segment = ®istration.segment;
384 let duration = segment.end_ms - segment.start_ms;
385 if duration == 0 || duration >= 240_000 {
386 return validation("segment duration must be 1..239999 ms");
387 }
388 if segment.ordinal == 0 && segment.start_ms != 0 {
389 return validation("source ordinal zero must begin at zero");
390 }
391 if self.registrations.contains_key(&segment.clip_object) {
392 return conflict("clip object is already registered");
393 }
394 for old in self
395 .registrations
396 .values()
397 .filter(|old| old.segment.source_object == segment.source_object)
398 {
399 if old.segment.policy != segment.policy
400 || old.segment.segment_count != segment.segment_count
401 || old.group_id != registration.group_id
402 || old.recording_kind != registration.recording_kind
403 {
404 return conflict("source plan, group, or recording kind differs");
405 }
406 if old.segment.ordinal == segment.ordinal {
407 return conflict("source ordinal is already registered");
408 }
409 let old_duration = old.segment.end_ms - old.segment.start_ms;
410 if old_duration.abs_diff(duration) > 1 {
411 return validation("source segment lengths differ by more than one millisecond");
412 }
413 let ordered = if old.segment.ordinal < segment.ordinal {
414 old.segment.start_ms < segment.start_ms && old.segment.end_ms < segment.end_ms
415 } else {
416 segment.start_ms < old.segment.start_ms && segment.end_ms < old.segment.end_ms
417 };
418 if !ordered {
419 return validation("source segment ranges do not follow ordinal order");
420 }
421 if old.segment.ordinal.checked_add(1) == Some(segment.ordinal)
422 && old.segment.end_ms.checked_sub(segment.start_ms) != Some(5_000)
423 {
424 return validation("adjacent source segments must overlap by 5000 ms");
425 }
426 if segment.ordinal.checked_add(1) == Some(old.segment.ordinal)
427 && segment.end_ms.checked_sub(old.segment.start_ms) != Some(5_000)
428 {
429 return validation("adjacent source segments must overlap by 5000 ms");
430 }
431 }
432 self.registrations
433 .insert(segment.clip_object.clone(), registration.clone());
434 Ok(())
435 }
436
437 fn record_attempt(&mut self, attempt: &AttemptRecord) -> Result<(), StoreError> {
438 if self.attempts.contains_key(&attempt.attempt_id) {
439 return conflict("attempt ID is already recorded");
440 }
441 let registration = self
442 .registrations
443 .get(&attempt.clip_object)
444 .ok_or_else(|| StoreError::Conflict("clip must be registered first".into()))?;
445 if !matches!(
446 attempt.outcome,
447 StoredAttemptOutcome::ProviderFailure { .. }
448 ) && attempt.provider_result_object.is_none()
449 {
450 return validation("response-bearing attempt needs a result object");
451 }
452 match &attempt.outcome {
453 StoredAttemptOutcome::Scored {
454 recording_quality,
455 speakers,
456 } => {
457 if *recording_quality > 100 || speakers.is_empty() {
458 return validation("scored attempt needs quality <=100 and speakers");
459 }
460 let duration = registration.segment.end_ms - registration.segment.start_ms;
461 for (index, speaker) in speakers.iter().enumerate() {
462 if usize::from(speaker.speaker_ordinal) != index {
463 return validation("speaker ordinals must be contiguous from zero");
464 }
465 bounded(&speaker.closest_dialect, 128, "dialect")?;
466 if speaker.usable_speech_ms == 0
467 || u64::from(speaker.usable_speech_ms) > duration
468 {
469 return validation("usable speech is outside segment duration");
470 }
471 sample_id(&attempt.attempt_id, speaker.speaker_ordinal)?;
472 }
473 }
474 StoredAttemptOutcome::Unscorable { reason } => bounded(reason, 240, "reason")?,
475 StoredAttemptOutcome::InvalidResponse { error } => bounded(error, 240, "error")?,
476 StoredAttemptOutcome::ProviderFailure { detail, .. } => bounded(detail, 240, "detail")?,
477 }
478 self.attempts
479 .insert(attempt.attempt_id.clone(), attempt.clone());
480 Ok(())
481 }
482
483 fn select(&mut self, selection: &AttemptSelection) -> Result<(), StoreError> {
484 bounded(&selection.reason, 240, "selection reason")?;
485 let attempt = self
486 .attempts
487 .get(&selection.attempt_id)
488 .ok_or_else(|| StoreError::Conflict("selected attempt does not exist".into()))?;
489 if attempt.clip_object != selection.clip_object
490 || attempt.extraction_key != selection.extraction_key
491 || !matches!(attempt.outcome, StoredAttemptOutcome::Scored { .. })
492 {
493 return conflict("selection does not match a scored clip attempt");
494 }
495 self.selections.insert(
496 (
497 selection.clip_object.clone(),
498 selection.extraction_key.clone(),
499 ),
500 selection.clone(),
501 );
502 Ok(())
503 }
504
505 fn set_sample_state(&mut self, change: &SampleStateChange) -> Result<(), StoreError> {
506 bounded(&change.reason, 240, "sample-state reason")?;
507 if self.sample_owner(&change.sample_id).is_none() {
508 return conflict("sample ID does not exist");
509 }
510 self.states.insert(change.sample_id.clone(), change.clone());
511 Ok(())
512 }
513
514 fn put_artifact(&mut self, artifact: &ArtifactRecord) -> Result<(), StoreError> {
515 let actual: [u8; 32] = Sha256::digest(&artifact.bytes).into();
516 if actual != artifact.sha256 {
517 return validation("artifact SHA-256 does not match bytes");
518 }
519 if let Some(old) = self.artifacts.get(&artifact.artifact_id) {
520 if old != artifact {
521 return conflict("artifact ID is immutable");
522 }
523 return Ok(());
524 }
525 self.artifacts
526 .insert(artifact.artifact_id.clone(), artifact.clone());
527 Ok(())
528 }
529
530 fn set_active_model(&mut self, change: &ActiveModelChange) -> Result<(), StoreError> {
531 bounded(&change.reason, 240, "activation reason")?;
532 if let Some(id) = &change.model_artifact_id {
533 let artifact = self
534 .artifacts
535 .get(id)
536 .ok_or_else(|| StoreError::Conflict("model artifact does not exist".into()))?;
537 if artifact.kind != ArtifactKind::Model || artifact.cohort_id != change.cohort_id {
538 return conflict("active artifact must be a Model in the same cohort");
539 }
540 }
541 self.active_models
542 .insert(change.cohort_id.clone(), change.clone());
543 Ok(())
544 }
545
546 fn sample_owner(&self, wanted: &Key) -> Option<(&AttemptRecord, &StoredSpeaker, u8)> {
547 for attempt in self.attempts.values() {
548 if let StoredAttemptOutcome::Scored {
549 recording_quality,
550 speakers,
551 } = &attempt.outcome
552 {
553 for speaker in speakers {
554 if sample_id(&attempt.attempt_id, speaker.speaker_ordinal)
555 .ok()
556 .as_ref()
557 == Some(wanted)
558 {
559 return Some((attempt, speaker, *recording_quality));
560 }
561 }
562 }
563 }
564 None
565 }
566
567 fn snapshot(&self, attempt: &AttemptRecord) -> Result<AttemptSnapshot, StoreError> {
568 let sample_ids = match &attempt.outcome {
569 StoredAttemptOutcome::Scored { speakers, .. } => speakers
570 .iter()
571 .map(|speaker| sample_id(&attempt.attempt_id, speaker.speaker_ordinal))
572 .collect::<Result<_, _>>()?,
573 _ => Vec::new(),
574 };
575 let selected = self
576 .selections
577 .get(&(attempt.clip_object.clone(), attempt.extraction_key.clone()))
578 .is_some_and(|selection| selection.attempt_id == attempt.attempt_id);
579 Ok(AttemptSnapshot {
580 attempt: attempt.clone(),
581 sample_ids,
582 selected,
583 })
584 }
585
586 fn rows(&self, cohort_id: &Key, selected_only: bool) -> Result<Vec<LabeledSample>, StoreError> {
587 let mut rows = Vec::new();
588 for attempt in self
589 .attempts
590 .values()
591 .filter(|attempt| &attempt.extraction_key == cohort_id)
592 {
593 if selected_only
594 && !self
595 .selections
596 .get(&(attempt.clip_object.clone(), attempt.extraction_key.clone()))
597 .is_some_and(|selection| selection.attempt_id == attempt.attempt_id)
598 {
599 continue;
600 }
601 let StoredAttemptOutcome::Scored {
602 recording_quality,
603 speakers,
604 } = &attempt.outcome
605 else {
606 continue;
607 };
608 let registration = self
609 .registrations
610 .get(&attempt.clip_object)
611 .ok_or_else(|| StoreError::Storage("attempt lost its registration".into()))?;
612 for speaker in speakers {
613 let id = sample_id(&attempt.attempt_id, speaker.speaker_ordinal)?;
614 let Some(SampleStateChange {
615 state: SampleState::Confirmed { speaker_id },
616 ..
617 }) = self.states.get(&id)
618 else {
619 continue;
620 };
621 rows.push(LabeledSample {
622 sample_id: id,
623 attempt_id: attempt.attempt_id.clone(),
624 speaker_id: speaker_id.clone(),
625 cohort_id: attempt.extraction_key.clone(),
626 group_id: registration.group_id.clone(),
627 clip_object: attempt.clip_object.clone(),
628 primary_language: speaker.primary_language.clone(),
629 recording_kind: registration.recording_kind,
630 usable_speech_ms: speaker.usable_speech_ms,
631 recording_quality: *recording_quality,
632 features: speaker.features.clone(),
633 });
634 }
635 }
636 Ok(rows)
637 }
638
639 fn read(&self, query: Query) -> Result<ResponseKind, StoreError> {
640 match query {
641 Query::Attempt { attempt_id } => Ok(ResponseKind::Attempt(
642 self.attempts
643 .get(&attempt_id)
644 .map(|attempt| self.snapshot(attempt))
645 .transpose()?,
646 )),
647 Query::Attempts {
648 extraction_key,
649 source_object,
650 } => Ok(ResponseKind::Attempts(
651 self.attempts
652 .values()
653 .filter(|attempt| {
654 extraction_key
655 .as_ref()
656 .is_none_or(|key| key == &attempt.extraction_key)
657 && source_object.as_ref().is_none_or(|source| {
658 self.registrations.get(&attempt.clip_object).is_some_and(
659 |registration| ®istration.segment.source_object == source,
660 )
661 })
662 })
663 .map(|attempt| self.snapshot(attempt))
664 .collect::<Result<_, _>>()?,
665 )),
666 Query::TrainingRows { cohort_id } => {
667 Ok(ResponseKind::TrainingRows(self.rows(&cohort_id, true)?))
668 }
669 Query::RepeatabilityRows { cohort_id } => Ok(ResponseKind::RepeatabilityRows(
670 self.rows(&cohort_id, false)?,
671 )),
672 Query::Artifact { artifact_id } => Ok(ResponseKind::Artifact(
673 self.artifacts.get(&artifact_id).cloned(),
674 )),
675 Query::ActiveModel { cohort_id } => Ok(ResponseKind::ActiveModel(
676 self.active_models
677 .get(&cohort_id)
678 .and_then(|change| change.model_artifact_id.clone()),
679 )),
680 Query::Inventory { extraction_key } => {
681 Ok(ResponseKind::Inventory(self.inventory(extraction_key)?))
682 }
683 }
684 }
685
686 fn inventory(&self, filter: Option<Key>) -> Result<InventorySnapshot, StoreError> {
687 let included = |attempt: &&AttemptRecord| {
688 filter
689 .as_ref()
690 .is_none_or(|key| key == &attempt.extraction_key)
691 };
692 let attempts = self
693 .attempts
694 .values()
695 .filter(included)
696 .map(|attempt| self.snapshot(attempt))
697 .collect::<Result<Vec<_>, _>>()?;
698 let included_samples: BTreeSet<Key> = attempts
699 .iter()
700 .flat_map(|attempt| attempt.sample_ids.iter().cloned())
701 .collect();
702 let selections = self
703 .selections
704 .values()
705 .filter(|selection| {
706 filter
707 .as_ref()
708 .is_none_or(|key| key == &selection.extraction_key)
709 })
710 .cloned()
711 .map(|selection| SelectionSnapshot { selection })
712 .collect();
713 let sample_states = self
714 .states
715 .values()
716 .filter(|change| filter.is_none() || included_samples.contains(&change.sample_id))
717 .cloned()
718 .map(|change| SampleStateSnapshot { change })
719 .collect();
720 let mut registrations: Vec<_> = self.registrations.values().cloned().collect();
721 registrations.sort_by(|a, b| {
722 (&a.segment.source_object, a.segment.ordinal)
723 .cmp(&(&b.segment.source_object, b.segment.ordinal))
724 });
725 let mut sources: BTreeMap<ObjectId, (u16, BTreeSet<u16>)> = BTreeMap::new();
726 for registration in ®istrations {
727 let entry = sources
728 .entry(registration.segment.source_object.clone())
729 .or_insert((registration.segment.segment_count, BTreeSet::new()));
730 entry.1.insert(registration.segment.ordinal);
731 }
732 let missing_segment_ordinals = sources
733 .into_iter()
734 .filter_map(|(source_object, (count, present))| {
735 let ordinals: Vec<_> = (0..count)
736 .filter(|ordinal| !present.contains(ordinal))
737 .collect();
738 (!ordinals.is_empty()).then_some(MissingSegmentOrdinals {
739 source_object,
740 ordinals,
741 })
742 })
743 .collect();
744 let mut outcome_counts = OutcomeCounts::default();
745 for attempt in self.attempts.values().filter(included) {
746 match attempt.outcome {
747 StoredAttemptOutcome::Scored { .. } => outcome_counts.scored += 1,
748 StoredAttemptOutcome::Unscorable { .. } => outcome_counts.unscorable += 1,
749 StoredAttemptOutcome::InvalidResponse { .. } => {
750 outcome_counts.invalid_response += 1
751 }
752 StoredAttemptOutcome::ProviderFailure { .. } => {
753 outcome_counts.provider_failure += 1
754 }
755 }
756 }
757 Ok(InventorySnapshot {
758 registrations,
759 attempts,
760 selections,
761 sample_states,
762 artifacts: self
763 .artifacts
764 .values()
765 .map(|artifact| ArtifactMetadata {
766 artifact_id: artifact.artifact_id.clone(),
767 cohort_id: artifact.cohort_id.clone(),
768 kind: artifact.kind,
769 sha256: artifact.sha256,
770 byte_len: artifact.bytes.len() as u64,
771 })
772 .collect(),
773 active_models: self
774 .active_models
775 .values()
776 .cloned()
777 .map(|change| ActiveModelSnapshot { change })
778 .collect(),
779 missing_segment_ordinals,
780 outcome_counts,
781 })
782 }
783}
784
785fn sample_id(attempt_id: &Key, ordinal: u16) -> Result<Key, StoreError> {
786 let mut digest = Sha256::new();
787 digest.update(attempt_id.as_ref().as_bytes());
788 digest.update([0]);
789 digest.update(ordinal.to_be_bytes());
790 let hex = digest
791 .finalize()
792 .iter()
793 .map(|byte| format!("{byte:02x}"))
794 .collect::<String>();
795 Key::parse(&format!("sample:sha256:{hex}"))
796 .map_err(|e| StoreError::Storage(format!("generated invalid sample ID: {e}")))
797}
798
799fn bounded(value: &str, maximum: usize, name: &str) -> Result<(), StoreError> {
800 if value.trim().is_empty() || value.len() > maximum {
801 return validation(format!(
802 "{name} must be nonblank and at most {maximum} bytes"
803 ));
804 }
805 Ok(())
806}
807
808fn validation<T>(message: impl Into<String>) -> Result<T, StoreError> {
809 Err(StoreError::Validation(message.into()))
810}
811
812fn conflict<T>(message: impl Into<String>) -> Result<T, StoreError> {
813 Err(StoreError::Conflict(message.into()))
814}
815
816#[cfg(test)]
817mod tests {
818 use super::*;
819 use std::{
820 fs,
821 path::PathBuf,
822 sync::{
823 Arc,
824 atomic::{AtomicU64, Ordering},
825 },
826 thread,
827 };
828
829 static NEXT_PATH: AtomicU64 = AtomicU64::new(0);
830
831 fn path() -> PathBuf {
832 std::env::temp_dir().join(format!(
833 "kcode-speaker-store-{}-{}.db",
834 std::process::id(),
835 NEXT_PATH.fetch_add(1, Ordering::Relaxed)
836 ))
837 }
838
839 fn key(value: &str) -> Key {
840 Key::parse(value).unwrap()
841 }
842
843 fn object(value: &str) -> ObjectId {
844 ObjectId::parse(value).unwrap()
845 }
846
847 fn features(seed: u8) -> FeatureVector {
848 FeatureVector::new([seed; 35]).unwrap()
849 }
850
851 fn registration(source: &str, clip: &str, ordinal: u16, count: u16) -> SegmentRegistration {
852 SegmentRegistration {
853 segment: SegmentRef {
854 source_object: object(source),
855 clip_object: object(clip),
856 ordinal,
857 segment_count: count,
858 start_ms: u64::from(ordinal) * 95_000,
859 end_ms: u64::from(ordinal) * 95_000 + 100_000,
860 policy: key("speaker-segments/1"),
861 },
862 group_id: key("group/1"),
863 recording_kind: RecordingKind::VoiceNote,
864 }
865 }
866
867 fn scored(id: &str, clip: &str, extraction: &str, speakers: u16) -> AttemptRecord {
868 AttemptRecord {
869 attempt_id: key(id),
870 clip_object: object(clip),
871 extraction_key: key(extraction),
872 provider_result_object: Some(object("RESULT01")),
873 outcome: StoredAttemptOutcome::Scored {
874 recording_quality: 80,
875 speakers: (0..speakers)
876 .map(|speaker_ordinal| StoredSpeaker {
877 speaker_ordinal,
878 primary_language: key("en"),
879 closest_dialect: "General American".into(),
880 usable_speech_ms: 20_000,
881 features: features(50),
882 })
883 .collect(),
884 },
885 }
886 }
887
888 fn failure(id: &str, clip: &str, extraction: &str) -> AttemptRecord {
889 AttemptRecord {
890 attempt_id: key(id),
891 clip_object: object(clip),
892 extraction_key: key(extraction),
893 provider_result_object: None,
894 outcome: StoredAttemptOutcome::ProviderFailure {
895 code: key("timeout"),
896 detail: "provider timed out".into(),
897 },
898 }
899 }
900
901 fn event(id: &str, event: Event) -> EventEnvelope {
902 EventEnvelope {
903 event_id: key(id),
904 event,
905 }
906 }
907
908 fn commit(store: &Store, events: Vec<EventEnvelope>) -> Response {
909 store.execute(Request::Commit(events)).unwrap()
910 }
911
912 fn attempt(store: &Store, id: &str) -> AttemptSnapshot {
913 let response = store
914 .execute(Request::Read(Query::Attempt {
915 attempt_id: key(id),
916 }))
917 .unwrap();
918 let ResponseKind::Attempt(Some(attempt)) = response.result else {
919 panic!("missing attempt")
920 };
921 attempt
922 }
923
924 fn selection(id: &str, clip: &str, extraction: &str) -> AttemptSelection {
925 AttemptSelection {
926 clip_object: object(clip),
927 extraction_key: key(extraction),
928 attempt_id: key(id),
929 reason: "reviewed".into(),
930 }
931 }
932
933 fn state(sample_id: Key, state: SampleState) -> SampleStateChange {
934 SampleStateChange {
935 sample_id,
936 state,
937 reason: "human review".into(),
938 }
939 }
940
941 #[test]
942 fn fresh_database_restarts() {
943 let path = path();
944 {
945 let store = open(&path).unwrap();
946 let response = commit(
947 &store,
948 vec![event(
949 "event/1",
950 Event::RegisterSegment(registration("SOURCE01", "CLIP0001", 0, 1)),
951 )],
952 );
953 assert_eq!(response.revision, 1);
954 }
955 let store = open(&path).unwrap();
956 let response = store
957 .execute(Request::Read(Query::Inventory {
958 extraction_key: None,
959 }))
960 .unwrap();
961 assert_eq!(response.revision, 1);
962 let ResponseKind::Inventory(inventory) = response.result else {
963 panic!()
964 };
965 assert_eq!(inventory.registrations.len(), 1);
966 drop(store);
967 fs::remove_file(path).unwrap();
968 }
969
970 #[test]
971 fn commit_is_atomic_and_event_ids_are_idempotent() {
972 let path = path();
973 let store = open(&path).unwrap();
974 let registration_event = event(
975 "event/register",
976 Event::RegisterSegment(registration("SOURCE01", "CLIP0001", 0, 1)),
977 );
978 let bad = event(
979 "event/bad",
980 Event::RecordAttempt(scored("attempt/1", "UNKNOWN1", "extract/1", 1)),
981 );
982 assert!(
983 store
984 .execute(Request::Commit(vec![registration_event.clone(), bad]))
985 .is_err()
986 );
987 let response = store
988 .execute(Request::Read(Query::Inventory {
989 extraction_key: None,
990 }))
991 .unwrap();
992 assert_eq!(response.revision, 0);
993
994 assert!(matches!(
995 commit(&store, vec![registration_event.clone()]).result,
996 ResponseKind::Commit { applied: 1 }
997 ));
998 assert!(matches!(
999 commit(
1000 &store,
1001 vec![registration_event.clone(), registration_event.clone()]
1002 )
1003 .result,
1004 ResponseKind::Commit { applied: 0 }
1005 ));
1006 let mut changed = registration_event;
1007 changed.event = Event::RegisterSegment(registration("SOURCE02", "CLIP0002", 0, 1));
1008 assert!(matches!(
1009 store.execute(Request::Commit(vec![changed])),
1010 Err(StoreError::Conflict(_))
1011 ));
1012 assert_eq!(
1013 store
1014 .execute(Request::Read(Query::Inventory {
1015 extraction_key: None
1016 }))
1017 .unwrap()
1018 .revision,
1019 1
1020 );
1021 drop(store);
1022 fs::remove_file(path).unwrap();
1023 }
1024
1025 #[test]
1026 fn registration_order_and_source_plan_conflicts_fail() {
1027 let path = path();
1028 let store = open(&path).unwrap();
1029 assert!(matches!(
1030 store.execute(Request::Commit(vec![event(
1031 "attempt/early",
1032 Event::RecordAttempt(scored("attempt/1", "CLIP0001", "extract/1", 1))
1033 )])),
1034 Err(StoreError::Conflict(_))
1035 ));
1036 commit(
1037 &store,
1038 vec![event(
1039 "register/0",
1040 Event::RegisterSegment(registration("SOURCE01", "CLIP0001", 0, 2)),
1041 )],
1042 );
1043 let mut wrong_count = registration("SOURCE01", "CLIP0002", 1, 3);
1044 assert!(
1045 store
1046 .execute(Request::Commit(vec![event(
1047 "register/wrong-count",
1048 Event::RegisterSegment(wrong_count.clone())
1049 )]))
1050 .is_err()
1051 );
1052 wrong_count.segment.segment_count = 2;
1053 wrong_count.segment.policy = key("different-plan");
1054 assert!(
1055 store
1056 .execute(Request::Commit(vec![event(
1057 "register/wrong-plan",
1058 Event::RegisterSegment(wrong_count)
1059 )]))
1060 .is_err()
1061 );
1062 let duplicate_ordinal = registration("SOURCE01", "CLIP0003", 0, 2);
1063 assert!(
1064 store
1065 .execute(Request::Commit(vec![event(
1066 "register/duplicate",
1067 Event::RegisterSegment(duplicate_ordinal)
1068 )]))
1069 .is_err()
1070 );
1071 drop(store);
1072 fs::remove_file(path).unwrap();
1073 }
1074
1075 #[test]
1076 fn retries_replacement_ids_and_label_projections() {
1077 let path = path();
1078 let store = open(&path).unwrap();
1079 commit(
1080 &store,
1081 vec![
1082 event(
1083 "register/1",
1084 Event::RegisterSegment(registration("SOURCE01", "CLIP0001", 0, 1)),
1085 ),
1086 event(
1087 "attempt/1",
1088 Event::RecordAttempt(scored("try/one", "CLIP0001", "extract/1", 2)),
1089 ),
1090 event(
1091 "attempt/2",
1092 Event::RecordAttempt(scored("try/two", "CLIP0001", "extract/1", 1)),
1093 ),
1094 event(
1095 "select/1",
1096 Event::SelectAttempt(selection("try/one", "CLIP0001", "extract/1")),
1097 ),
1098 ],
1099 );
1100 let first = attempt(&store, "try/one");
1101 assert_eq!(first.sample_ids.len(), 2);
1102 assert_ne!(first.sample_ids[0], first.sample_ids[1]);
1103 let mut digest = Sha256::new();
1104 digest.update(b"try/one");
1105 digest.update([0]);
1106 digest.update(0u16.to_be_bytes());
1107 let expected = key(&format!("sample:sha256:{:x}", digest.finalize()));
1108 assert_eq!(first.sample_ids[0], expected);
1109
1110 let second = attempt(&store, "try/two");
1111 commit(
1112 &store,
1113 vec![
1114 event(
1115 "state/1",
1116 Event::SetSampleState(state(
1117 first.sample_ids[0].clone(),
1118 SampleState::Confirmed {
1119 speaker_id: key("speaker/alice"),
1120 },
1121 )),
1122 ),
1123 event(
1124 "state/2",
1125 Event::SetSampleState(state(
1126 second.sample_ids[0].clone(),
1127 SampleState::Confirmed {
1128 speaker_id: key("speaker/alice"),
1129 },
1130 )),
1131 ),
1132 ],
1133 );
1134 let training = store
1135 .execute(Request::Read(Query::TrainingRows {
1136 cohort_id: key("extract/1"),
1137 }))
1138 .unwrap();
1139 let ResponseKind::TrainingRows(rows) = training.result else {
1140 panic!()
1141 };
1142 assert_eq!(rows.len(), 1);
1143 assert_eq!(rows[0].attempt_id, key("try/one"));
1144 let repeat = store
1145 .execute(Request::Read(Query::RepeatabilityRows {
1146 cohort_id: key("extract/1"),
1147 }))
1148 .unwrap();
1149 let ResponseKind::RepeatabilityRows(rows) = repeat.result else {
1150 panic!()
1151 };
1152 assert_eq!(rows.len(), 2);
1153
1154 commit(
1155 &store,
1156 vec![event(
1157 "select/2",
1158 Event::SelectAttempt(selection("try/two", "CLIP0001", "extract/1")),
1159 )],
1160 );
1161 let ResponseKind::TrainingRows(rows) = store
1162 .execute(Request::Read(Query::TrainingRows {
1163 cohort_id: key("extract/1"),
1164 }))
1165 .unwrap()
1166 .result
1167 else {
1168 panic!()
1169 };
1170 assert_eq!(rows[0].attempt_id, key("try/two"));
1171
1172 commit(
1173 &store,
1174 vec![event(
1175 "state/correct",
1176 Event::SetSampleState(state(
1177 second.sample_ids[0].clone(),
1178 SampleState::Confirmed {
1179 speaker_id: key("speaker/bob"),
1180 },
1181 )),
1182 )],
1183 );
1184 let ResponseKind::TrainingRows(rows) = store
1185 .execute(Request::Read(Query::TrainingRows {
1186 cohort_id: key("extract/1"),
1187 }))
1188 .unwrap()
1189 .result
1190 else {
1191 panic!()
1192 };
1193 assert_eq!(rows[0].speaker_id, key("speaker/bob"));
1194 commit(
1195 &store,
1196 vec![event(
1197 "state/retract",
1198 Event::SetSampleState(state(second.sample_ids[0].clone(), SampleState::Retracted)),
1199 )],
1200 );
1201 let ResponseKind::TrainingRows(rows) = store
1202 .execute(Request::Read(Query::TrainingRows {
1203 cohort_id: key("extract/1"),
1204 }))
1205 .unwrap()
1206 .result
1207 else {
1208 panic!()
1209 };
1210 assert!(rows.is_empty());
1211 drop(store);
1212 fs::remove_file(path).unwrap();
1213 }
1214
1215 #[test]
1216 fn artifacts_are_hashed_immutable_and_cohort_checked() {
1217 let path = path();
1218 let store = open(&path).unwrap();
1219 let bytes = b"model bytes".to_vec();
1220 let hash: [u8; 32] = Sha256::digest(&bytes).into();
1221 let model = ArtifactRecord {
1222 artifact_id: key("artifact/model"),
1223 cohort_id: key("cohort/1"),
1224 kind: ArtifactKind::Model,
1225 sha256: hash,
1226 bytes,
1227 };
1228 let evaluation = ArtifactRecord {
1229 artifact_id: key("artifact/eval"),
1230 cohort_id: key("cohort/1"),
1231 kind: ArtifactKind::Evaluation,
1232 sha256: Sha256::digest(b"evaluation").into(),
1233 bytes: b"evaluation".to_vec(),
1234 };
1235 commit(
1236 &store,
1237 vec![
1238 event("artifact/model", Event::PutArtifact(model.clone())),
1239 event("artifact/eval", Event::PutArtifact(evaluation)),
1240 ],
1241 );
1242 let activate = |id: &str, cohort: &str, artifact: &str| {
1243 event(
1244 id,
1245 Event::SetActiveModel(ActiveModelChange {
1246 cohort_id: key(cohort),
1247 model_artifact_id: Some(key(artifact)),
1248 reason: "approved".into(),
1249 }),
1250 )
1251 };
1252 assert!(
1253 store
1254 .execute(Request::Commit(vec![activate(
1255 "activate/wrong-kind",
1256 "cohort/1",
1257 "artifact/eval"
1258 )]))
1259 .is_err()
1260 );
1261 assert!(
1262 store
1263 .execute(Request::Commit(vec![activate(
1264 "activate/wrong-cohort",
1265 "cohort/2",
1266 "artifact/model"
1267 )]))
1268 .is_err()
1269 );
1270
1271 let mut changed = model.clone();
1272 changed.bytes.push(1);
1273 changed.sha256 = Sha256::digest(&changed.bytes).into();
1274 assert!(matches!(
1275 store.execute(Request::Commit(vec![event(
1276 "artifact/change",
1277 Event::PutArtifact(changed)
1278 )])),
1279 Err(StoreError::Conflict(_))
1280 ));
1281 let mut bad_hash = model;
1282 bad_hash.artifact_id = key("artifact/bad");
1283 bad_hash.sha256 = [0; 32];
1284 assert!(matches!(
1285 store.execute(Request::Commit(vec![event(
1286 "artifact/bad",
1287 Event::PutArtifact(bad_hash)
1288 )])),
1289 Err(StoreError::Validation(_))
1290 ));
1291 commit(
1292 &store,
1293 vec![activate("activate/good", "cohort/1", "artifact/model")],
1294 );
1295 let ResponseKind::ActiveModel(Some(active)) = store
1296 .execute(Request::Read(Query::ActiveModel {
1297 cohort_id: key("cohort/1"),
1298 }))
1299 .unwrap()
1300 .result
1301 else {
1302 panic!()
1303 };
1304 assert_eq!(active, key("artifact/model"));
1305 commit(
1306 &store,
1307 vec![event(
1308 "activate/none",
1309 Event::SetActiveModel(ActiveModelChange {
1310 cohort_id: key("cohort/1"),
1311 model_artifact_id: None,
1312 reason: "retired".into(),
1313 }),
1314 )],
1315 );
1316 assert!(matches!(
1317 store
1318 .execute(Request::Read(Query::ActiveModel {
1319 cohort_id: key("cohort/1")
1320 }))
1321 .unwrap()
1322 .result,
1323 ResponseKind::ActiveModel(None)
1324 ));
1325 drop(store);
1326 fs::remove_file(path).unwrap();
1327 }
1328
1329 #[test]
1330 fn filtering_inventory_missing_and_counts_are_typed() {
1331 let path = path();
1332 let store = open(&path).unwrap();
1333 let unscorable = AttemptRecord {
1334 attempt_id: key("unscorable"),
1335 clip_object: object("CLIP0001"),
1336 extraction_key: key("extract/1"),
1337 provider_result_object: Some(object("RESULT02")),
1338 outcome: StoredAttemptOutcome::Unscorable {
1339 reason: "too faint".into(),
1340 },
1341 };
1342 let invalid = AttemptRecord {
1343 attempt_id: key("invalid"),
1344 clip_object: object("CLIP0001"),
1345 extraction_key: key("extract/1"),
1346 provider_result_object: Some(object("RESULT03")),
1347 outcome: StoredAttemptOutcome::InvalidResponse {
1348 error: "extra field".into(),
1349 },
1350 };
1351 commit(
1352 &store,
1353 vec![
1354 event(
1355 "register",
1356 Event::RegisterSegment(registration("SOURCE01", "CLIP0001", 0, 2)),
1357 ),
1358 event(
1359 "attempt/scored",
1360 Event::RecordAttempt(scored("scored", "CLIP0001", "extract/1", 1)),
1361 ),
1362 event("attempt/unscorable", Event::RecordAttempt(unscorable)),
1363 event("attempt/invalid", Event::RecordAttempt(invalid)),
1364 event(
1365 "attempt/failure",
1366 Event::RecordAttempt(failure("failure", "CLIP0001", "extract/1")),
1367 ),
1368 event(
1369 "attempt/other",
1370 Event::RecordAttempt(failure("other", "CLIP0001", "extract/2")),
1371 ),
1372 ],
1373 );
1374 let ResponseKind::Attempts(filtered) = store
1375 .execute(Request::Read(Query::Attempts {
1376 extraction_key: Some(key("extract/1")),
1377 source_object: Some(object("SOURCE01")),
1378 }))
1379 .unwrap()
1380 .result
1381 else {
1382 panic!()
1383 };
1384 assert_eq!(filtered.len(), 4);
1385 let ResponseKind::Inventory(inventory) = store
1386 .execute(Request::Read(Query::Inventory {
1387 extraction_key: Some(key("extract/1")),
1388 }))
1389 .unwrap()
1390 .result
1391 else {
1392 panic!()
1393 };
1394 assert_eq!(inventory.attempts.len(), 4);
1395 assert_eq!(
1396 inventory.outcome_counts,
1397 OutcomeCounts {
1398 scored: 1,
1399 unscorable: 1,
1400 invalid_response: 1,
1401 provider_failure: 1,
1402 }
1403 );
1404 assert_eq!(inventory.missing_segment_ordinals.len(), 1);
1405 assert_eq!(inventory.missing_segment_ordinals[0].ordinals, vec![1]);
1406 drop(store);
1407 fs::remove_file(path).unwrap();
1408 }
1409
1410 #[test]
1411 fn future_schema_is_rejected() {
1412 let path = path();
1413 let connection = Connection::open(&path).unwrap();
1414 connection
1415 .execute_batch("PRAGMA user_version = 2;")
1416 .unwrap();
1417 drop(connection);
1418 assert!(matches!(open(&path), Err(StoreError::SchemaVersion(2))));
1419 fs::remove_file(path).unwrap();
1420 }
1421
1422 #[test]
1423 fn one_store_serializes_concurrent_calls() {
1424 let path = path();
1425 let store = Arc::new(open(&path).unwrap());
1426 commit(
1427 &store,
1428 vec![event(
1429 "register",
1430 Event::RegisterSegment(registration("SOURCE01", "CLIP0001", 0, 1)),
1431 )],
1432 );
1433 let threads: Vec<_> = (0..8)
1434 .map(|index| {
1435 let store = Arc::clone(&store);
1436 thread::spawn(move || {
1437 commit(
1438 &store,
1439 vec![event(
1440 &format!("event/{index}"),
1441 Event::RecordAttempt(failure(
1442 &format!("attempt/{index}"),
1443 "CLIP0001",
1444 "extract/1",
1445 )),
1446 )],
1447 );
1448 })
1449 })
1450 .collect();
1451 for thread in threads {
1452 thread.join().unwrap();
1453 }
1454 let response = store
1455 .execute(Request::Read(Query::Attempts {
1456 extraction_key: None,
1457 source_object: None,
1458 }))
1459 .unwrap();
1460 assert_eq!(response.revision, 9);
1461 let ResponseKind::Attempts(attempts) = response.result else {
1462 panic!()
1463 };
1464 assert_eq!(attempts.len(), 8);
1465 drop(store);
1466 fs::remove_file(path).unwrap();
1467 }
1468}