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