Skip to main content

kcode_speaker_store/
lib.rs

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 = &registration.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| &registration.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 &registrations {
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}