Skip to main content

kcode_audio_session_ingress/
lib.rs

1#![forbid(unsafe_code)]
2
3use std::collections::{HashMap, HashSet};
4
5use chrono::{DateTime, Utc};
6use kcode_audio_ingress::{
7    AudioIngress, AudioInput, ConfirmationState, CorrectionPacket, ErrorKind as AudioErrorKind,
8    RecordingConfirmation, RecordingState, RecordingStatus,
9};
10use kcode_session_history::{
11    NewIngressSession, RetryIngress as HistoryRetryIngress, SessionHistory, SessionRecord,
12    chatend::SessionKind,
13};
14use serde_json::{Value, json};
15use sha2::{Digest, Sha256};
16use uuid::Uuid;
17
18const ESTIMATED_CHARACTERS_PER_TOKEN: u64 = 4;
19const INGRESS_CONTEXT_DIVISOR: u64 = 4;
20const SEGMENTATION_VERSION: u64 = 1;
21
22/// Application policy needed to coordinate audio with Session History.
23#[derive(Clone, Debug)]
24pub struct Config {
25    /// Stable application user identifier attributed to accepted recordings.
26    pub user_id: String,
27    /// Effective context window available to an audio-ingress session.
28    pub effective_context_tokens: u64,
29}
30
31/// Admitted recording bytes and source metadata.
32#[derive(Clone, Debug)]
33pub struct RecordingInput {
34    /// Complete WAV bytes already accepted by the transport.
35    pub bytes: Vec<u8>,
36    /// Instant at which the original recording began.
37    pub recorded_at: DateTime<Utc>,
38    /// Original leaf filename, when known.
39    pub original_filename: Option<String>,
40}
41
42/// Result of durably submitting one recording.
43#[derive(Clone, Debug)]
44pub struct RecordingSubmission {
45    /// Current combined state after submission.
46    pub recording: Recording,
47    /// Whether AudioIngress already knew the same bytes.
48    pub deduplicated: bool,
49}
50
51/// Combined recording and memory-ingress status.
52#[derive(Clone, Debug)]
53pub struct Recording {
54    pub id: Uuid,
55    pub sha256: String,
56    pub original_filename: String,
57    pub content_type: &'static str,
58    pub size_bytes: u64,
59    pub source_created_at: String,
60    pub received_at: String,
61    pub updated_at: String,
62    pub status: String,
63    pub transcription_model: String,
64    pub reconciliation_model: String,
65    pub reconciliation_reasoning: String,
66    pub transcription_status: Option<Value>,
67    pub attempt_count: i64,
68    pub next_attempt_at: Option<String>,
69    pub last_error: Option<String>,
70    /// Current classifier-aware human-review state, absent for legacy recordings.
71    pub speaker_review: Option<SpeakerReview>,
72    /// Total number of pieces already represented in Session History.
73    pub transcript_piece_count: usize,
74    /// Number of represented pieces whose Session History ingress is complete.
75    pub completed_piece_count: usize,
76}
77
78/// Bounded review summary for one classifier-aware recording.
79#[derive(Clone, Debug)]
80pub struct SpeakerReview {
81    pub clean: bool,
82    pub confirmation_state: ConfirmationState,
83    pub observation_count: usize,
84}
85
86/// One deterministic transcript piece and its Session History lifecycle.
87#[derive(Clone, Debug)]
88pub struct IngressPiece {
89    pub id: String,
90    pub recording_id: Uuid,
91    pub sha256: String,
92    pub original_filename: String,
93    pub source_created_at: String,
94    pub piece_index: u32,
95    pub piece_count: u32,
96    pub transcript_text: String,
97    pub estimated_tokens: u64,
98    pub phase: String,
99    pub provenance_id: Option<String>,
100    pub state: Value,
101    pub version: i64,
102    pub ingress_failure_count: i64,
103    pub ingress_failures: Value,
104    pub created_at: String,
105    pub updated_at: String,
106}
107
108/// Detailed state for one recording.
109#[derive(Clone, Debug)]
110pub struct RecordingHistory {
111    pub recording: Recording,
112    pub final_transcript: Option<String>,
113    pub correction_packet: Option<CorrectionPacket>,
114    pub pieces: Vec<IngressPiece>,
115}
116
117/// Request to retry one transcript piece's memory ingress.
118#[derive(Clone, Debug)]
119pub struct RetryIngress {
120    pub piece_id: String,
121    pub expected_version: i64,
122    /// Optional replacement state retained for wire compatibility with the
123    /// pre-extraction Kennedy endpoint.
124    pub state: Option<Value>,
125}
126
127/// Stable coordinator error category for transport mapping.
128#[derive(Clone, Copy, Debug, Eq, PartialEq)]
129pub enum ErrorKind {
130    InvalidInput,
131    NotFound,
132    Conflict,
133    Internal,
134}
135
136/// Error returned by the audio/session-ingress coordinator.
137#[derive(Debug)]
138pub struct Error {
139    kind: ErrorKind,
140    message: String,
141}
142
143impl Error {
144    fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
145        Self {
146            kind,
147            message: message.into(),
148        }
149    }
150
151    fn invalid(message: impl Into<String>) -> Self {
152        Self::new(ErrorKind::InvalidInput, message)
153    }
154
155    fn conflict(message: impl Into<String>) -> Self {
156        Self::new(ErrorKind::Conflict, message)
157    }
158
159    fn not_found() -> Self {
160        Self::new(
161            ErrorKind::NotFound,
162            "Audio recording or transcript piece not found.",
163        )
164    }
165
166    fn internal(error: impl std::fmt::Display) -> Self {
167        tracing::warn!(%error, "Audio session ingress operation failed");
168        Self::new(
169            ErrorKind::Internal,
170            "An unexpected audio session ingress error occurred.",
171        )
172    }
173
174    /// Returns the stable error category.
175    pub fn kind(&self) -> ErrorKind {
176        self.kind
177    }
178
179    /// Returns the sanitized actionable message.
180    pub fn message(&self) -> &str {
181        &self.message
182    }
183}
184
185impl std::fmt::Display for Error {
186    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
187        formatter.write_str(&self.message)
188    }
189}
190
191impl std::error::Error for Error {}
192
193/// Cloneable typed coordinator over AudioIngress and Session History.
194#[derive(Clone)]
195pub struct Coordinator {
196    audio: AudioIngress,
197    history: SessionHistory,
198    user_id: String,
199    effective_context_tokens: u64,
200    maximum_piece_characters: usize,
201}
202
203impl Coordinator {
204    /// Constructs a coordinator over already-opened capability handles.
205    pub fn new(
206        audio: AudioIngress,
207        history: SessionHistory,
208        config: Config,
209    ) -> Result<Self, Error> {
210        if config.user_id.trim().is_empty() {
211            return Err(Error::invalid("audio user ID must not be empty"));
212        }
213        let maximum_piece_characters = maximum_piece_characters(config.effective_context_tokens)?;
214        Ok(Self {
215            audio,
216            history,
217            user_id: config.user_id,
218            effective_context_tokens: config.effective_context_tokens,
219            maximum_piece_characters,
220        })
221    }
222
223    /// Checks that AudioIngress can read its current state.
224    pub fn health(&self) -> Result<(), Error> {
225        self.audio.status().map_err(audio_error)?;
226        Ok(())
227    }
228
229    /// Durably submits one admitted recording and returns its combined state.
230    pub async fn submit(&self, input: RecordingInput) -> Result<RecordingSubmission, Error> {
231        let submission = self
232            .audio
233            .submit(AudioInput {
234                user_id: self.user_id.clone(),
235                bytes: input.bytes,
236                recorded_at: input.recorded_at,
237                original_filename: input.original_filename,
238            })
239            .await
240            .map_err(audio_error)?;
241        let recording_status = self
242            .audio
243            .status()
244            .map_err(audio_error)?
245            .recordings
246            .into_iter()
247            .find(|recording| recording.id == submission.recording_id)
248            .ok_or_else(Error::not_found)?;
249        let histories = self.history.list().await.map_err(history_error)?;
250        let projection = ingress_projection(
251            &recording_status,
252            &histories,
253            self.effective_context_tokens,
254            self.maximum_piece_characters,
255        )?;
256        let recording = Recording::from_status(recording_status, &projection);
257        Ok(RecordingSubmission {
258            recording,
259            deduplicated: submission.deduplicated,
260        })
261    }
262
263    /// Returns recordings with their correlated ingress state.
264    pub async fn recordings(&self) -> Result<Vec<Recording>, Error> {
265        let histories = self.history.list().await.map_err(history_error)?;
266        self.audio
267            .status()
268            .map_err(audio_error)?
269            .recordings
270            .into_iter()
271            .map(|recording| {
272                let projection = ingress_projection(
273                    &recording,
274                    &histories,
275                    self.effective_context_tokens,
276                    self.maximum_piece_characters,
277                )?;
278                Ok(Recording::from_status(recording, &projection))
279            })
280            .collect()
281    }
282
283    /// Finds a recording by its SHA-256 digest.
284    pub async fn recording_by_sha256(&self, sha256: &str) -> Result<Recording, Error> {
285        if sha256.len() != 64 || !sha256.bytes().all(|byte| byte.is_ascii_hexdigit()) {
286            return Err(Error::invalid(
287                "audio SHA-256 must contain exactly 64 hexadecimal characters",
288            ));
289        }
290        let normalized = sha256.to_ascii_lowercase();
291        self.recordings()
292            .await?
293            .into_iter()
294            .find(|recording| recording.sha256 == normalized)
295            .ok_or_else(Error::not_found)
296    }
297
298    /// Returns one recording and its correlated pieces.
299    pub async fn recording_history(&self, recording_id: Uuid) -> Result<RecordingHistory, Error> {
300        let recording_status = self
301            .audio
302            .status()
303            .map_err(audio_error)?
304            .recordings
305            .into_iter()
306            .find(|recording| recording.id == recording_id)
307            .ok_or_else(Error::not_found)?;
308        let histories = self.history.list().await.map_err(history_error)?;
309        let projection = ingress_projection(
310            &recording_status,
311            &histories,
312            self.effective_context_tokens,
313            self.maximum_piece_characters,
314        )?;
315        let final_transcript = match &recording_status.state {
316            RecordingState::Complete { transcript } => Some(transcript.clone()),
317            _ => None,
318        };
319        let correction_packet = recording_status.correction_packet.clone();
320        let recording = Recording::from_status(recording_status, &projection);
321        Ok(RecordingHistory {
322            recording,
323            final_transcript,
324            correction_packet,
325            pieces: projection.pieces,
326        })
327    }
328
329    /// Gives one failed recording a fresh processing budget.
330    pub fn retry_recording(&self, recording_id: Uuid) -> Result<(), Error> {
331        self.audio.retry(recording_id).map_err(audio_error)
332    }
333
334    /// Applies the complete human-confirmed speaker mapping and immediately
335    /// attempts the now-authorized Session History handoff.
336    pub async fn confirm_speakers(
337        &self,
338        confirmation: RecordingConfirmation,
339    ) -> Result<CorrectionPacket, Error> {
340        let recording_id = confirmation.recording_id;
341        let recording = self
342            .audio
343            .status()
344            .map_err(audio_error)?
345            .recordings
346            .into_iter()
347            .find(|recording| recording.id == recording_id)
348            .ok_or_else(Error::not_found)?;
349        let histories = self.history.list().await.map_err(history_error)?;
350        if let Some(packet) = recording
351            .correction_packet
352            .as_ref()
353            .filter(|packet| confirmation_matches(packet, &confirmation))
354        {
355            synchronize_recordings(
356                &self.history,
357                std::slice::from_ref(&recording),
358                self.effective_context_tokens,
359                self.maximum_piece_characters,
360            )
361            .await?;
362            return Ok(packet.clone());
363        }
364        if recording_has_ingress(recording_id, &histories) {
365            return Err(Error::conflict(
366                "speaker labels are already bound to accepted transcript ingress",
367            ));
368        }
369        let packet = self
370            .audio
371            .confirm_speakers(confirmation)
372            .map_err(audio_error)?;
373        let recording = self
374            .audio
375            .status()
376            .map_err(audio_error)?
377            .recordings
378            .into_iter()
379            .find(|recording| recording.id == recording_id)
380            .ok_or_else(Error::not_found)?;
381        synchronize_recordings(
382            &self.history,
383            std::slice::from_ref(&recording),
384            self.effective_context_tokens,
385            self.maximum_piece_characters,
386        )
387        .await?;
388        Ok(packet)
389    }
390
391    /// Retries one Session History ingress record.
392    pub async fn retry_ingress(&self, input: RetryIngress) -> Result<SessionRecord, Error> {
393        let current = self
394            .history
395            .get(&input.piece_id)
396            .await
397            .map_err(history_error)?;
398        self.history
399            .retry_ingress(
400                &input.piece_id,
401                HistoryRetryIngress {
402                    expected_version: input.expected_version,
403                    state: input.state.unwrap_or(current.state),
404                },
405            )
406            .await
407            .map_err(history_error)
408    }
409
410    /// Submits every missing transcript piece to Session History.
411    pub async fn synchronize_completed_transcripts(&self) -> Result<(), Error> {
412        let recordings = self
413            .audio
414            .status()
415            .map_err(audio_error)?
416            .recordings
417            .into_iter()
418            .collect::<Vec<_>>();
419        synchronize_recordings(
420            &self.history,
421            &recordings,
422            self.effective_context_tokens,
423            self.maximum_piece_characters,
424        )
425        .await
426    }
427}
428
429fn confirmation_matches(packet: &CorrectionPacket, confirmation: &RecordingConfirmation) -> bool {
430    if packet.confirmation_state != ConfirmationState::Confirmed {
431        return false;
432    }
433    let existing = packet
434        .chunks
435        .iter()
436        .flat_map(|chunk| &chunk.observations)
437        .filter_map(|observation| {
438            observation
439                .confirmed_full_name
440                .as_deref()
441                .map(|name| (&observation.observation_key, name))
442        })
443        .collect::<HashMap<_, _>>();
444    existing.len() == confirmation.observations.len()
445        && confirmation.observations.iter().all(|observation| {
446            existing.get(&observation.observation_key).copied()
447                == Some(observation.confirmed_full_name.trim())
448        })
449}
450
451fn recording_has_ingress(recording_id: Uuid, histories: &[SessionRecord]) -> bool {
452    let prefix = format!("audio:{recording_id}:");
453    histories.iter().any(|record| {
454        ingress_source_id(record).is_some_and(|source_id| source_id.starts_with(&prefix))
455    })
456}
457
458#[derive(Debug)]
459struct PieceSpec {
460    id: String,
461    index: u32,
462    count: u32,
463    text: String,
464}
465
466#[derive(Clone, Debug, Eq, PartialEq)]
467struct SegmentationParameters {
468    version: u64,
469    effective_context_tokens: u64,
470    maximum_piece_characters: usize,
471    transcript_sha256: String,
472    fingerprint: String,
473}
474
475#[derive(Debug)]
476struct SegmentationPlan {
477    parameters: SegmentationParameters,
478    specs: Vec<PieceSpec>,
479}
480
481#[derive(Debug)]
482struct PersistedPieceIdentity {
483    index: u32,
484    count: u32,
485    piece_characters: usize,
486    piece_text_sha256: String,
487    ingress_text_sha256: String,
488    parameters: SegmentationParameters,
489}
490
491#[derive(Debug, Default)]
492struct IngressProjection {
493    pieces: Vec<IngressPiece>,
494}
495
496impl Recording {
497    fn from_status(recording: RecordingStatus, projection: &IngressProjection) -> Self {
498        let speaker_review = recording
499            .correction_packet
500            .as_ref()
501            .map(|packet| SpeakerReview {
502                clean: packet.clean,
503                confirmation_state: packet.confirmation_state,
504                observation_count: packet
505                    .chunks
506                    .iter()
507                    .map(|chunk| chunk.observations.len())
508                    .sum(),
509            });
510        let awaiting_speaker_review = speaker_review
511            .as_ref()
512            .is_some_and(|review| review.confirmation_state != ConfirmationState::Confirmed);
513        let (mut status, transcription_status, attempt_count, last_error) = match recording.state {
514            RecordingState::Queued => ("uploaded".into(), None, 0, None),
515            RecordingState::Processing { attempt, progress } => (
516                processing_stage(&progress).into(),
517                serde_json::to_value(progress).ok(),
518                i64::from(attempt),
519                None,
520            ),
521            RecordingState::Complete { .. } if awaiting_speaker_review => {
522                ("speaker_review".into(), None, 0, None)
523            }
524            RecordingState::Complete { .. } => ("ready_for_ingress".into(), None, 0, None),
525            RecordingState::Failed {
526                attempts, error, ..
527            } => ("failed".into(), None, i64::from(attempts), Some(error)),
528        };
529        if !projection.pieces.is_empty() {
530            status = if projection
531                .pieces
532                .iter()
533                .all(|piece| piece.phase == "complete")
534            {
535                "complete".into()
536            } else if projection
537                .pieces
538                .iter()
539                .any(|piece| piece.phase == "ingress_failed")
540            {
541                "ingress_failed".into()
542            } else if projection
543                .pieces
544                .iter()
545                .any(|piece| piece.phase == "ingress_in_progress")
546            {
547                "ingressing".into()
548            } else {
549                "ready_for_ingress".into()
550            };
551        }
552        let completed_piece_count = projection
553            .pieces
554            .iter()
555            .filter(|piece| piece.phase == "complete")
556            .count();
557        Self {
558            id: recording.id,
559            sha256: recording.sha256,
560            original_filename: recording.original_filename,
561            content_type: "audio/wav",
562            size_bytes: recording.size_bytes,
563            source_created_at: recording.recorded_at.to_rfc3339(),
564            received_at: recording.received_at.to_rfc3339(),
565            updated_at: recording.received_at.to_rfc3339(),
566            status,
567            transcription_model: recording.transcription_model,
568            reconciliation_model: recording.reconciliation_model,
569            reconciliation_reasoning: recording.reconciliation_reasoning,
570            transcription_status,
571            attempt_count,
572            next_attempt_at: None,
573            last_error,
574            speaker_review,
575            transcript_piece_count: projection.pieces.len(),
576            completed_piece_count,
577        }
578    }
579}
580
581impl IngressPiece {
582    fn from_record(recording: &RecordingStatus, spec: &PieceSpec, record: &SessionRecord) -> Self {
583        Self {
584            id: record.id.clone(),
585            recording_id: recording.id,
586            sha256: recording.sha256.clone(),
587            original_filename: recording.original_filename.clone(),
588            source_created_at: recording.recorded_at.to_rfc3339(),
589            piece_index: spec.index,
590            piece_count: spec.count,
591            estimated_tokens: estimate_tokens(&spec.text),
592            transcript_text: spec.text.clone(),
593            phase: record.phase.clone(),
594            provenance_id: record.provenance_id.clone(),
595            state: record.state.clone(),
596            version: record.version,
597            ingress_failure_count: record.ingress_failure_count,
598            ingress_failures: record.ingress_failures.clone(),
599            created_at: record.started_at.clone(),
600            updated_at: record.updated_at.clone(),
601        }
602    }
603}
604
605async fn synchronize_recordings(
606    history: &SessionHistory,
607    recordings: &[RecordingStatus],
608    effective_context_tokens: u64,
609    maximum_piece_characters: usize,
610) -> Result<(), Error> {
611    let histories = history.list().await.map_err(history_error)?;
612    let mut existing = histories
613        .iter()
614        .filter_map(ingress_source_id)
615        .map(str::to_owned)
616        .collect::<HashSet<_>>();
617    for recording in recordings {
618        if !matches!(&recording.state, RecordingState::Complete { .. })
619            || !speaker_labels_authorized(recording)
620        {
621            continue;
622        }
623        let plan = segmentation_plan(
624            recording,
625            &histories,
626            effective_context_tokens,
627            maximum_piece_characters,
628        )?;
629        for spec in &plan.specs {
630            if existing.contains(&spec.id) {
631                continue;
632            }
633            let text = format_ingress_piece(recording, spec)?;
634            let created = history
635                .enqueue_ingress(NewIngressSession {
636                    idempotency_id: spec.id.clone(),
637                    started_at: recording.recorded_at.to_rfc3339(),
638                    source_session_type: "audio".into(),
639                    kind: SessionKind::AudioIngress,
640                    effective_context_tokens: plan.parameters.effective_context_tokens,
641                    text: text.clone(),
642                    metadata: audio_piece_metadata(recording, spec, &plan.parameters, &text),
643                })
644                .await
645                .map_err(history_error)?;
646            if !created.created {
647                validate_persisted_record(recording, &plan.parameters, spec, &created.value)?;
648            }
649            existing.insert(spec.id.clone());
650        }
651    }
652    Ok(())
653}
654
655fn speaker_labels_authorized(recording: &RecordingStatus) -> bool {
656    recording
657        .correction_packet
658        .as_ref()
659        .is_none_or(|packet| packet.confirmation_state == ConfirmationState::Confirmed)
660}
661
662fn ingress_projection(
663    recording: &RecordingStatus,
664    histories: &[SessionRecord],
665    effective_context_tokens: u64,
666    maximum_piece_characters: usize,
667) -> Result<IngressProjection, Error> {
668    if !matches!(&recording.state, RecordingState::Complete { .. }) {
669        return Ok(IngressProjection::default());
670    }
671    let plan = segmentation_plan(
672        recording,
673        histories,
674        effective_context_tokens,
675        maximum_piece_characters,
676    )?;
677    let mut pieces = Vec::with_capacity(plan.specs.len());
678    for spec in &plan.specs {
679        let Some(record) = histories
680            .iter()
681            .find(|record| ingress_source_id(record) == Some(spec.id.as_str()))
682        else {
683            continue;
684        };
685        pieces.push(IngressPiece::from_record(recording, spec, record));
686    }
687    Ok(IngressProjection { pieces })
688}
689
690fn segmentation_plan(
691    recording: &RecordingStatus,
692    histories: &[SessionRecord],
693    effective_context_tokens: u64,
694    current_maximum_piece_characters: usize,
695) -> Result<SegmentationPlan, Error> {
696    let persisted = persisted_audio_records(recording.id, histories)?;
697    if persisted.is_empty() {
698        return new_segmentation_plan(
699            recording,
700            effective_context_tokens,
701            current_maximum_piece_characters,
702        );
703    }
704
705    let mut identities = Vec::with_capacity(persisted.len());
706    for (index, record) in persisted {
707        identities.push((record, persisted_piece_identity(recording, index, record)?));
708    }
709    let parameters = identities[0].1.parameters.clone();
710    if identities
711        .iter()
712        .any(|(_, identity)| identity.parameters != parameters)
713    {
714        return Err(Error::conflict(
715            "persisted audio pieces disagree about authoritative segmentation",
716        ));
717    }
718    let expected_maximum = maximum_piece_characters(parameters.effective_context_tokens)
719        .map_err(|_| Error::conflict("persisted audio segmentation parameters are invalid"))?;
720    if expected_maximum != parameters.maximum_piece_characters {
721        return Err(Error::conflict(
722            "persisted audio segmentation parameters are inconsistent",
723        ));
724    }
725    if parameters.version != SEGMENTATION_VERSION
726        || parameters.fingerprint != segmentation_fingerprint(&parameters)
727    {
728        return Err(Error::conflict(
729            "persisted audio segmentation authority is unsupported or invalid",
730        ));
731    }
732
733    let transcript = completed_transcript(recording)?;
734    if sha256_text(transcript.trim()) != parameters.transcript_sha256 {
735        return Err(Error::conflict(
736            "completed audio transcript conflicts with persisted ingress identity",
737        ));
738    }
739    let specs = transcript_piece_specs(recording, parameters.maximum_piece_characters)?;
740    for (record, identity) in &identities {
741        let spec = specs.get(identity.index as usize).ok_or_else(|| {
742            Error::conflict("persisted audio piece index is outside authoritative segmentation")
743        })?;
744        validate_persisted_identity(recording, &parameters, spec, identity)?;
745        if ingress_source_id(record) != Some(spec.id.as_str()) {
746            return Err(Error::conflict(
747                "persisted audio piece identity does not match its segmentation index",
748            ));
749        }
750    }
751    Ok(SegmentationPlan { parameters, specs })
752}
753
754fn new_segmentation_plan(
755    recording: &RecordingStatus,
756    effective_context_tokens: u64,
757    current_maximum_piece_characters: usize,
758) -> Result<SegmentationPlan, Error> {
759    if maximum_piece_characters(effective_context_tokens)? != current_maximum_piece_characters {
760        return Err(Error::internal(
761            "audio ingress context and piece limit are inconsistent",
762        ));
763    }
764    let transcript = completed_transcript(recording)?;
765    let mut parameters = SegmentationParameters {
766        version: SEGMENTATION_VERSION,
767        effective_context_tokens,
768        maximum_piece_characters: current_maximum_piece_characters,
769        transcript_sha256: sha256_text(transcript.trim()),
770        fingerprint: String::new(),
771    };
772    parameters.fingerprint = segmentation_fingerprint(&parameters);
773    let specs = transcript_piece_specs(recording, current_maximum_piece_characters)?;
774    Ok(SegmentationPlan { parameters, specs })
775}
776
777fn persisted_audio_records(
778    recording_id: Uuid,
779    histories: &[SessionRecord],
780) -> Result<Vec<(u32, &SessionRecord)>, Error> {
781    let prefix = format!("audio:{recording_id}:");
782    let mut records = Vec::new();
783    let mut indexes = HashSet::new();
784    for record in histories {
785        let Some(source_id) = ingress_source_id(record) else {
786            continue;
787        };
788        let Some(suffix) = source_id.strip_prefix(&prefix) else {
789            continue;
790        };
791        let index = suffix.parse::<u32>().map_err(|_| {
792            Error::conflict("persisted audio piece has an invalid deterministic identity")
793        })?;
794        if audio_piece_id(recording_id, index) != source_id {
795            return Err(Error::conflict(
796                "persisted audio piece has a noncanonical deterministic identity",
797            ));
798        }
799        if !indexes.insert(index) {
800            return Err(Error::conflict(
801                "multiple persisted audio pieces claim the same deterministic identity",
802            ));
803        }
804        records.push((index, record));
805    }
806    records.sort_by_key(|(index, _)| *index);
807    Ok(records)
808}
809
810fn persisted_piece_identity(
811    recording: &RecordingStatus,
812    id_index: u32,
813    record: &SessionRecord,
814) -> Result<PersistedPieceIdentity, Error> {
815    let metadata = record
816        .state
817        .pointer("/ingressSource/metadata")
818        .and_then(Value::as_object)
819        .ok_or_else(|| {
820            Error::conflict("persisted audio piece lacks authoritative segmentation metadata")
821        })?;
822    let recording_id = recording.id.to_string();
823    let source_created_at = recording.recorded_at.to_rfc3339();
824    if metadata.get("kind").and_then(Value::as_str) != Some("audio-transcript")
825        || metadata.get("recordingId").and_then(Value::as_str) != Some(recording_id.as_str())
826        || metadata.get("sha256").and_then(Value::as_str) != Some(recording.sha256.as_str())
827        || metadata.get("originalFilename").and_then(Value::as_str)
828            != Some(recording.original_filename.as_str())
829        || metadata.get("sizeBytes").and_then(Value::as_u64) != Some(recording.size_bytes)
830        || metadata.get("sourceCreatedAt").and_then(Value::as_str)
831            != Some(source_created_at.as_str())
832    {
833        return Err(Error::conflict(
834            "persisted audio piece metadata conflicts with its recording",
835        ));
836    }
837
838    let version = required_metadata_u64(metadata, "segmentationVersion")?;
839    let effective_context_tokens = required_metadata_u64(metadata, "effectiveContextTokens")?;
840    let maximum_piece_characters =
841        usize::try_from(required_metadata_u64(metadata, "maximumPieceCharacters")?)
842            .map_err(|_| Error::conflict("persisted audio piece limit exceeds platform bounds"))?;
843    let index = u32::try_from(required_metadata_u64(metadata, "pieceIndex")?)
844        .map_err(|_| Error::conflict("persisted audio piece index exceeds u32"))?;
845    let count = u32::try_from(required_metadata_u64(metadata, "pieceCount")?)
846        .map_err(|_| Error::conflict("persisted audio piece count exceeds u32"))?;
847    let piece_characters = usize::try_from(required_metadata_u64(metadata, "pieceCharacters")?)
848        .map_err(|_| Error::conflict("persisted audio piece length exceeds platform bounds"))?;
849    if index != id_index || count == 0 || index >= count {
850        return Err(Error::conflict(
851            "persisted audio piece index or count conflicts with its identity",
852        ));
853    }
854
855    let transcript_sha256 = required_metadata_hash(metadata, "transcriptSha256")?;
856    let fingerprint = required_metadata_hash(metadata, "segmentationFingerprint")?;
857    let piece_text_sha256 = required_metadata_hash(metadata, "pieceTextSha256")?;
858    let ingress_text_sha256 = required_metadata_hash(metadata, "ingressTextSha256")?;
859    Ok(PersistedPieceIdentity {
860        index,
861        count,
862        piece_characters,
863        piece_text_sha256,
864        ingress_text_sha256,
865        parameters: SegmentationParameters {
866            version,
867            effective_context_tokens,
868            maximum_piece_characters,
869            transcript_sha256,
870            fingerprint,
871        },
872    })
873}
874
875fn validate_persisted_record(
876    recording: &RecordingStatus,
877    parameters: &SegmentationParameters,
878    spec: &PieceSpec,
879    record: &SessionRecord,
880) -> Result<(), Error> {
881    let source_id = ingress_source_id(record).ok_or_else(|| {
882        Error::conflict("replayed audio ingress omitted its deterministic identity")
883    })?;
884    if source_id != spec.id {
885        return Err(Error::conflict(
886            "replayed audio ingress returned a different deterministic identity",
887        ));
888    }
889    let identity = persisted_piece_identity(recording, spec.index, record)?;
890    validate_persisted_identity(recording, parameters, spec, &identity)
891}
892
893fn validate_persisted_identity(
894    recording: &RecordingStatus,
895    parameters: &SegmentationParameters,
896    spec: &PieceSpec,
897    identity: &PersistedPieceIdentity,
898) -> Result<(), Error> {
899    if &identity.parameters != parameters
900        || identity.index != spec.index
901        || identity.count != spec.count
902        || identity.piece_characters != spec.text.chars().count()
903        || identity.piece_text_sha256 != sha256_text(&spec.text)
904    {
905        return Err(Error::conflict(
906            "persisted audio piece conflicts with authoritative transcript segmentation",
907        ));
908    }
909    let ingress_text = format_ingress_piece(recording, spec)?;
910    if identity.ingress_text_sha256 != sha256_text(&ingress_text) {
911        return Err(Error::conflict(
912            "persisted audio ingress text conflicts with its authoritative identity",
913        ));
914    }
915    Ok(())
916}
917
918fn required_metadata_u64(
919    metadata: &serde_json::Map<String, Value>,
920    field: &str,
921) -> Result<u64, Error> {
922    metadata.get(field).and_then(Value::as_u64).ok_or_else(|| {
923        Error::conflict(format!(
924            "persisted audio piece lacks valid {field} metadata"
925        ))
926    })
927}
928
929fn required_metadata_hash(
930    metadata: &serde_json::Map<String, Value>,
931    field: &str,
932) -> Result<String, Error> {
933    let value = metadata.get(field).and_then(Value::as_str).ok_or_else(|| {
934        Error::conflict(format!(
935            "persisted audio piece lacks valid {field} metadata"
936        ))
937    })?;
938    if value.len() != 64
939        || !value
940            .bytes()
941            .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
942    {
943        return Err(Error::conflict(format!(
944            "persisted audio piece has invalid {field} metadata"
945        )));
946    }
947    Ok(value.to_owned())
948}
949
950fn completed_transcript(recording: &RecordingStatus) -> Result<&str, Error> {
951    let RecordingState::Complete { transcript } = &recording.state else {
952        return Err(Error::internal(
953            "audio segmentation requested for an incomplete recording",
954        ));
955    };
956    Ok(transcript)
957}
958
959fn transcript_piece_specs(
960    recording: &RecordingStatus,
961    maximum_piece_characters: usize,
962) -> Result<Vec<PieceSpec>, Error> {
963    let RecordingState::Complete { transcript } = &recording.state else {
964        return Ok(Vec::new());
965    };
966    let pieces = split_transcript(transcript, maximum_piece_characters)?;
967    let piece_count = u32::try_from(pieces.len())
968        .map_err(|_| Error::internal("audio transcript contains too many pieces"))?;
969    pieces
970        .into_iter()
971        .enumerate()
972        .map(|(index, text)| {
973            let piece_index = u32::try_from(index)
974                .map_err(|_| Error::internal("audio transcript piece index exceeds u32"))?;
975            Ok(PieceSpec {
976                id: audio_piece_id(recording.id, piece_index),
977                index: piece_index,
978                count: piece_count,
979                text,
980            })
981        })
982        .collect()
983}
984
985fn ingress_source_id(record: &SessionRecord) -> Option<&str> {
986    record
987        .state
988        .pointer("/ingressSource/idempotencyId")
989        .and_then(Value::as_str)
990}
991
992fn audio_piece_id(recording_id: Uuid, piece_index: u32) -> String {
993    format!("audio:{recording_id}:{piece_index}")
994}
995
996fn audio_piece_metadata(
997    recording: &RecordingStatus,
998    spec: &PieceSpec,
999    parameters: &SegmentationParameters,
1000    ingress_text: &str,
1001) -> Value {
1002    json!({
1003        "kind":"audio-transcript",
1004        "recordingId":recording.id.to_string(),
1005        "sha256":recording.sha256,
1006        "originalFilename":recording.original_filename,
1007        "extension":file_name_extension(&recording.original_filename),
1008        "mimeType":"audio/wav",
1009        "sizeBytes":recording.size_bytes,
1010        "sourceCreatedAt":recording.recorded_at.to_rfc3339(),
1011        "pieceIndex":spec.index,
1012        "pieceCount":spec.count,
1013        "pieceCharacters":spec.text.chars().count(),
1014        "segmentationVersion":parameters.version,
1015        "effectiveContextTokens":parameters.effective_context_tokens,
1016        "maximumPieceCharacters":parameters.maximum_piece_characters,
1017        "transcriptSha256":parameters.transcript_sha256,
1018        "segmentationFingerprint":parameters.fingerprint,
1019        "pieceTextSha256":sha256_text(&spec.text),
1020        "ingressTextSha256":sha256_text(ingress_text),
1021        "speakerConfirmationState":recording.correction_packet.as_ref().map(|packet| match packet.confirmation_state {
1022            ConfirmationState::Unconfirmed => "unconfirmed",
1023            ConfirmationState::AutomaticallyTrained => "automatically_trained",
1024            ConfirmationState::Confirmed => "confirmed",
1025        }),
1026    })
1027}
1028
1029fn segmentation_fingerprint(parameters: &SegmentationParameters) -> String {
1030    sha256_text(&format!(
1031        "kcode-audio-session-ingress-segmentation\n{}\n{}\n{}\n{}",
1032        parameters.version,
1033        parameters.effective_context_tokens,
1034        parameters.maximum_piece_characters,
1035        parameters.transcript_sha256,
1036    ))
1037}
1038
1039fn sha256_text(value: &str) -> String {
1040    format!("{:x}", Sha256::digest(value.as_bytes()))
1041}
1042
1043fn format_ingress_piece(recording: &RecordingStatus, spec: &PieceSpec) -> Result<String, Error> {
1044    let speaker_mapping = confirmed_speaker_mapping(recording)?;
1045    Ok(format!(
1046        "Vnote final transcript piece\n\nRecording began: {}\nRecording SHA-256: {}\nOriginal filename: {}\nExtension: {}\nMIME type: audio/wav\nSize: {} bytes\nTranscript piece: {} of {}{}\n\n{}",
1047        recording.recorded_at.to_rfc3339(),
1048        recording.sha256,
1049        recording.original_filename,
1050        file_name_extension(&recording.original_filename),
1051        recording.size_bytes,
1052        spec.index + 1,
1053        spec.count,
1054        speaker_mapping,
1055        spec.text,
1056    ))
1057}
1058
1059fn confirmed_speaker_mapping(recording: &RecordingStatus) -> Result<String, Error> {
1060    let Some(packet) = recording.correction_packet.as_ref() else {
1061        return Ok(String::new());
1062    };
1063    if packet.confirmation_state != ConfirmationState::Confirmed {
1064        return Err(Error::conflict(
1065            "audio speaker labels require exact human confirmation before ingress",
1066        ));
1067    }
1068    let mut lines = Vec::new();
1069    for chunk in &packet.chunks {
1070        for observation in &chunk.observations {
1071            let confirmed = observation.confirmed_full_name.as_deref().ok_or_else(|| {
1072                Error::internal("confirmed audio packet omitted an observation label")
1073            })?;
1074            let local_label =
1075                serde_json::to_string(&observation.local_label).map_err(Error::internal)?;
1076            let confirmed = serde_json::to_string(confirmed).map_err(Error::internal)?;
1077            let candidate = observation
1078                .identified_full_name
1079                .as_deref()
1080                .map(serde_json::to_string)
1081                .transpose()
1082                .map_err(Error::internal)?
1083                .unwrap_or_else(|| "null".into());
1084            lines.push(format!(
1085                "- chunk {}/{}, source {:.3}-{:.3}s: localLabel={}, confirmedFullName={}, classifierCandidate={}",
1086                chunk.chunk_index + 1,
1087                chunk.chunk_count,
1088                chunk.audio_start_ms as f64 / 1_000.0,
1089                chunk.audio_end_ms as f64 / 1_000.0,
1090                local_label,
1091                confirmed,
1092                candidate,
1093            ));
1094        }
1095    }
1096    if lines.is_empty() {
1097        return Err(Error::internal(
1098            "confirmed audio packet contains no speaker observations",
1099        ));
1100    }
1101    Ok(format!(
1102        "\n\nHuman-confirmed speaker-label data (authoritative; do not infer alternatives):\n{}",
1103        lines.join("\n")
1104    ))
1105}
1106
1107fn file_name_extension(file_name: &str) -> String {
1108    file_name
1109        .rsplit_once('.')
1110        .and_then(|(stem, extension)| {
1111            (!stem.is_empty() && !extension.is_empty()).then_some(extension)
1112        })
1113        .map(|extension| format!(".{extension}"))
1114        .unwrap_or_else(|| "(none)".into())
1115}
1116
1117fn processing_stage(status: &kcode_audio_ingress::TranscriptionStatus) -> &'static str {
1118    let plan_complete = status.steps.iter().any(|entry| {
1119        entry.step == kcode_audio_ingress::Step::PlanChunks
1120            && entry.state == kcode_audio_ingress::StepState::Completed
1121    });
1122    if !plan_complete {
1123        return "chunking";
1124    }
1125    let chunks_complete = status
1126        .steps
1127        .iter()
1128        .filter(|entry| {
1129            matches!(
1130                entry.step,
1131                kcode_audio_ingress::Step::TranscribeChunk { .. }
1132            )
1133        })
1134        .all(|entry| entry.state == kcode_audio_ingress::StepState::Completed);
1135    if chunks_complete {
1136        let analyses_complete = status
1137            .steps
1138            .iter()
1139            .filter(|entry| matches!(entry.step, kcode_audio_ingress::Step::ParseChunk { .. }))
1140            .all(|entry| entry.state == kcode_audio_ingress::StepState::Completed);
1141        if !analyses_complete {
1142            return "analyzing_speakers";
1143        }
1144        let training_active = status.steps.iter().any(|entry| {
1145            entry.step == kcode_audio_ingress::Step::TrainIdentities
1146                && matches!(
1147                    entry.state,
1148                    kcode_audio_ingress::StepState::Running
1149                        | kcode_audio_ingress::StepState::Retrying
1150                )
1151        });
1152        if training_active {
1153            "training_speakers"
1154        } else {
1155            "reconciling"
1156        }
1157    } else {
1158        "transcribing"
1159    }
1160}
1161
1162fn maximum_piece_characters(effective_context_tokens: u64) -> Result<usize, Error> {
1163    let piece_tokens = effective_context_tokens / INGRESS_CONTEXT_DIVISOR;
1164    if piece_tokens == 0 {
1165        return Err(Error::invalid(
1166            "effective ingress context must contain at least four tokens",
1167        ));
1168    }
1169    let characters = piece_tokens
1170        .checked_mul(ESTIMATED_CHARACTERS_PER_TOKEN)
1171        .ok_or_else(|| Error::invalid("effective ingress context is too large"))?;
1172    usize::try_from(characters)
1173        .map_err(|_| Error::invalid("effective ingress context exceeds platform limits"))
1174}
1175
1176fn split_transcript(
1177    transcript: &str,
1178    maximum_piece_characters: usize,
1179) -> Result<Vec<String>, Error> {
1180    let mut remaining = transcript.trim();
1181    if remaining.is_empty() {
1182        return Err(Error::internal("completed audio transcript is empty"));
1183    }
1184    let mut pieces = Vec::new();
1185    while remaining.chars().count() > maximum_piece_characters {
1186        let cutoff = remaining
1187            .char_indices()
1188            .nth(maximum_piece_characters)
1189            .map(|(index, _)| index)
1190            .unwrap_or(remaining.len());
1191        let prefix = &remaining[..cutoff];
1192        let minimum = prefix
1193            .char_indices()
1194            .nth(maximum_piece_characters / 2)
1195            .map(|(index, _)| index)
1196            .unwrap_or(0);
1197        let boundary = prefix
1198            .rfind("\n\n")
1199            .filter(|index| *index >= minimum)
1200            .or_else(|| prefix.rfind('\n').filter(|index| *index >= minimum))
1201            .unwrap_or(cutoff);
1202        let piece = remaining[..boundary].trim();
1203        if piece.is_empty() {
1204            return Err(Error::internal("could not split audio transcript"));
1205        }
1206        pieces.push(piece.to_owned());
1207        remaining = remaining[boundary..].trim();
1208    }
1209    if !remaining.is_empty() {
1210        pieces.push(remaining.to_owned());
1211    }
1212    Ok(pieces)
1213}
1214
1215fn estimate_tokens(value: &str) -> u64 {
1216    (value.chars().count() as u64).div_ceil(ESTIMATED_CHARACTERS_PER_TOKEN)
1217}
1218
1219fn audio_error(error: kcode_audio_ingress::Error) -> Error {
1220    match error.kind() {
1221        AudioErrorKind::InvalidInput => Error::new(ErrorKind::InvalidInput, error.to_string()),
1222        AudioErrorKind::NotFound => Error::new(ErrorKind::NotFound, error.to_string()),
1223        AudioErrorKind::Conflict => Error::new(ErrorKind::Conflict, error.to_string()),
1224        AudioErrorKind::Internal => Error::internal(error),
1225    }
1226}
1227
1228fn history_error(error: kcode_session_history::Error) -> Error {
1229    let kind = match error.kind {
1230        kcode_session_history::ErrorKind::InvalidInput => ErrorKind::InvalidInput,
1231        kcode_session_history::ErrorKind::NotFound => ErrorKind::NotFound,
1232        kcode_session_history::ErrorKind::Conflict => ErrorKind::Conflict,
1233        kcode_session_history::ErrorKind::Storage => ErrorKind::Internal,
1234    };
1235    Error::new(kind, error.message)
1236}
1237
1238#[cfg(test)]
1239mod tests {
1240    use std::path::{Path, PathBuf};
1241
1242    use super::*;
1243
1244    struct TestRoot(PathBuf);
1245
1246    impl TestRoot {
1247        fn new() -> Self {
1248            let path = std::env::temp_dir().join(format!(
1249                "kcode-audio-session-ingress-test-{}",
1250                Uuid::new_v4()
1251            ));
1252            std::fs::create_dir(&path).unwrap();
1253            Self(path)
1254        }
1255
1256        fn path(&self) -> &Path {
1257            &self.0
1258        }
1259    }
1260
1261    impl Drop for TestRoot {
1262        fn drop(&mut self) {
1263            let _ = std::fs::remove_dir_all(&self.0);
1264        }
1265    }
1266
1267    fn history(root: &Path) -> SessionHistory {
1268        SessionHistory::open(kcode_session_history::Config {
1269            directory: root.join("active"),
1270            completed_list: root.join("completed.txt"),
1271        })
1272        .unwrap()
1273    }
1274
1275    fn completed_recording(transcript: impl Into<String>) -> RecordingStatus {
1276        let now = Utc::now();
1277        RecordingStatus {
1278            id: Uuid::new_v4(),
1279            user_id: "user".into(),
1280            sha256: "0".repeat(64),
1281            original_filename: "meeting.final.WAV".into(),
1282            size_bytes: 42,
1283            recorded_at: now,
1284            received_at: now,
1285            transcription_model: "transcription-model".into(),
1286            reconciliation_model: "reconciliation-model".into(),
1287            reconciliation_reasoning: "xhigh".into(),
1288            state: RecordingState::Complete {
1289                transcript: transcript.into(),
1290            },
1291            correction_packet: None,
1292        }
1293    }
1294
1295    fn with_speaker_packet(
1296        mut recording: RecordingStatus,
1297        state: ConfirmationState,
1298        confirmed_name: Option<&str>,
1299    ) -> RecordingStatus {
1300        recording.correction_packet = Some(CorrectionPacket {
1301            recording_id: recording.id,
1302            user_id: recording.user_id.clone(),
1303            sha256: recording.sha256.clone(),
1304            original_filename: recording.original_filename.clone(),
1305            size_bytes: recording.size_bytes,
1306            recorded_at: recording.recorded_at,
1307            clean: true,
1308            chunk_count: 1,
1309            chunks: vec![kcode_audio_ingress::CorrectionChunk {
1310                chunk_index: 0,
1311                chunk_count: 1,
1312                audio_start_ms: 0,
1313                audio_end_ms: 1_000,
1314                raw_gemini_response: "raw".into(),
1315                parsed: kcode_audio_ingress::ParsedChunk {
1316                    utterances: Vec::new(),
1317                    notes: Vec::new(),
1318                    clip_valid: true,
1319                    clip_validity_reason: None,
1320                    speakers: Vec::new(),
1321                },
1322                observations: vec![kcode_audio_ingress::CorrectionObservation {
1323                    local_label: "Speaker A".into(),
1324                    speaker_ordinal: 0,
1325                    observation_key: kcode_audio_ingress::ObservationKey {
1326                        object_id: format!(
1327                            "kcode-audio-ingress/recording/{}/chunk/0",
1328                            recording.id
1329                        ),
1330                        piece_index: 0,
1331                    },
1332                    candidate: Some(kcode_audio_ingress::CandidateMapping {
1333                        full_name: "Classifier Candidate".into(),
1334                        cost: 1.0,
1335                        confidence: 3.0,
1336                        runner_up_full_name: None,
1337                        runner_up_cost: None,
1338                        background_population_cost: 4.0,
1339                    }),
1340                    identified_full_name: Some("Classifier Candidate".into()),
1341                    confirmed_full_name: confirmed_name.map(str::to_owned),
1342                }],
1343                clean: true,
1344            }],
1345            confirmation_state: state,
1346        });
1347        recording
1348    }
1349
1350    #[test]
1351    fn transcript_piece_limit_is_one_quarter_of_effective_context() {
1352        let effective_context_tokens = 400;
1353        let maximum_characters = maximum_piece_characters(effective_context_tokens).unwrap();
1354        let pieces = split_transcript(&"a".repeat(801), maximum_characters).unwrap();
1355        assert_eq!(maximum_characters, 400);
1356        assert_eq!(pieces.len(), 3);
1357        assert!(pieces.iter().all(|piece| {
1358            estimate_tokens(piece) <= effective_context_tokens / INGRESS_CONTEXT_DIVISOR
1359        }));
1360    }
1361
1362    #[test]
1363    fn transcript_splitting_prefers_a_late_paragraph_boundary() {
1364        let transcript = format!("{}\n\n{}", "a".repeat(250), "b".repeat(200));
1365        let pieces = split_transcript(&transcript, 400).unwrap();
1366        assert_eq!(pieces, vec!["a".repeat(250), "b".repeat(200)]);
1367    }
1368
1369    #[test]
1370    fn transcript_splitting_counts_unicode_scalars_not_bytes() {
1371        let transcript = "😀".repeat(5);
1372        let pieces = split_transcript(&transcript, 2).unwrap();
1373        assert_eq!(pieces.concat(), transcript);
1374        assert!(pieces.iter().all(|piece| piece.chars().count() <= 2));
1375    }
1376
1377    #[test]
1378    fn audio_piece_ids_are_stable_across_configuration() {
1379        let recording_id = Uuid::new_v4();
1380        assert_eq!(
1381            audio_piece_id(recording_id, 2),
1382            format!("audio:{recording_id}:2")
1383        );
1384    }
1385
1386    #[test]
1387    fn confirmed_label_retries_must_match_the_complete_bound_mapping() {
1388        let recording = with_speaker_packet(
1389            completed_recording("Transcript"),
1390            ConfirmationState::Confirmed,
1391            Some("Human Choice"),
1392        );
1393        let packet = recording.correction_packet.as_ref().unwrap();
1394        let observation_key = packet.chunks[0].observations[0].observation_key.clone();
1395        let matching = RecordingConfirmation {
1396            recording_id: recording.id,
1397            observations: vec![kcode_audio_ingress::ObservationConfirmation {
1398                observation_key: observation_key.clone(),
1399                confirmed_full_name: " Human Choice ".into(),
1400            }],
1401        };
1402        let conflicting = RecordingConfirmation {
1403            recording_id: recording.id,
1404            observations: vec![kcode_audio_ingress::ObservationConfirmation {
1405                observation_key,
1406                confirmed_full_name: "Different Choice".into(),
1407            }],
1408        };
1409
1410        assert!(confirmation_matches(packet, &matching));
1411        assert!(!confirmation_matches(packet, &conflicting));
1412    }
1413
1414    #[test]
1415    fn ingress_exposes_the_complete_file_and_segmentation_contract() {
1416        let recording = completed_recording("Transcript");
1417        let plan = new_segmentation_plan(&recording, 400, 400).unwrap();
1418        let spec = &plan.specs[0];
1419        let text = format_ingress_piece(&recording, spec).unwrap();
1420        let metadata = audio_piece_metadata(&recording, spec, &plan.parameters, &text);
1421        assert_eq!(metadata["kind"], "audio-transcript");
1422        assert_eq!(metadata["recordingId"], recording.id.to_string());
1423        assert_eq!(metadata["sha256"], recording.sha256);
1424        assert_eq!(metadata["originalFilename"], "meeting.final.WAV");
1425        assert_eq!(metadata["extension"], ".WAV");
1426        assert_eq!(metadata["mimeType"], "audio/wav");
1427        assert_eq!(metadata["sizeBytes"], 42);
1428        assert_eq!(
1429            metadata["sourceCreatedAt"],
1430            recording.recorded_at.to_rfc3339()
1431        );
1432        assert_eq!(metadata["pieceIndex"], 0);
1433        assert_eq!(metadata["pieceCount"], 1);
1434        assert_eq!(metadata["pieceCharacters"], 10);
1435        assert_eq!(metadata["segmentationVersion"], SEGMENTATION_VERSION);
1436        assert_eq!(metadata["effectiveContextTokens"], 400);
1437        assert_eq!(metadata["maximumPieceCharacters"], 400);
1438        assert_eq!(metadata["pieceTextSha256"], sha256_text("Transcript"));
1439        assert_eq!(metadata["ingressTextSha256"], sha256_text(&text));
1440        assert_eq!(metadata["speakerConfirmationState"], Value::Null);
1441        assert!(text.contains("Original filename: meeting.final.WAV"));
1442        assert!(text.contains("Extension: .WAV"));
1443        assert!(text.contains("MIME type: audio/wav"));
1444        assert!(text.contains("Size: 42 bytes"));
1445    }
1446
1447    #[test]
1448    fn reads_report_only_existing_pieces_before_worker_synchronization() {
1449        let recording = completed_recording("a".repeat(801));
1450        let projection = ingress_projection(&recording, &[], 400, 400).unwrap();
1451        assert!(projection.pieces.is_empty());
1452        let combined = Recording::from_status(recording, &projection);
1453        assert_eq!(combined.status, "ready_for_ingress");
1454        assert_eq!(combined.attempt_count, 0);
1455        assert_eq!(combined.transcript_piece_count, 0);
1456        assert_eq!(combined.completed_piece_count, 0);
1457    }
1458
1459    #[tokio::test]
1460    async fn classifier_aware_recordings_wait_for_exact_human_review() {
1461        let root = TestRoot::new();
1462        let history = history(root.path());
1463        let recording = with_speaker_packet(
1464            completed_recording("Transcript"),
1465            ConfirmationState::AutomaticallyTrained,
1466            None,
1467        );
1468
1469        let combined = Recording::from_status(recording.clone(), &IngressProjection::default());
1470        assert_eq!(combined.status, "speaker_review");
1471        assert_eq!(combined.speaker_review.unwrap().observation_count, 1);
1472
1473        synchronize_recordings(&history, &[recording], 400, 400)
1474            .await
1475            .unwrap();
1476        assert!(history.list().await.unwrap().is_empty());
1477    }
1478
1479    #[tokio::test]
1480    async fn confirmed_labels_authorize_ingress_and_accompany_the_transcript() {
1481        let root = TestRoot::new();
1482        let history = history(root.path());
1483        let recording = with_speaker_packet(
1484            completed_recording("Transcript"),
1485            ConfirmationState::Confirmed,
1486            Some("Human Choice"),
1487        );
1488
1489        synchronize_recordings(&history, std::slice::from_ref(&recording), 400, 400)
1490            .await
1491            .unwrap();
1492        let records = history.list().await.unwrap();
1493        assert_eq!(records.len(), 1);
1494        assert_eq!(
1495            records[0]
1496                .state
1497                .pointer("/ingressSource/metadata/speakerConfirmationState")
1498                .and_then(Value::as_str),
1499            Some("confirmed")
1500        );
1501        let state = serde_json::to_string(&records[0].state).unwrap();
1502        assert!(state.contains("Human-confirmed speaker-label data"));
1503        assert!(state.contains("Human Choice"));
1504        assert!(state.contains("Classifier Candidate"));
1505    }
1506
1507    #[tokio::test]
1508    async fn synchronization_is_idempotent_and_uses_quarter_window_pieces() {
1509        let root = TestRoot::new();
1510        let history = history(root.path());
1511        let recording = completed_recording("a".repeat(801));
1512
1513        synchronize_recordings(&history, std::slice::from_ref(&recording), 400, 400)
1514            .await
1515            .unwrap();
1516        synchronize_recordings(&history, std::slice::from_ref(&recording), 400, 400)
1517            .await
1518            .unwrap();
1519
1520        let records = history.list().await.unwrap();
1521        assert_eq!(records.len(), 3);
1522        let ids = records
1523            .iter()
1524            .filter_map(ingress_source_id)
1525            .collect::<HashSet<_>>();
1526        assert_eq!(ids.len(), 3);
1527        assert!(ids.contains(format!("audio:{}:0", recording.id).as_str()));
1528        assert!(ids.contains(format!("audio:{}:1", recording.id).as_str()));
1529        assert!(ids.contains(format!("audio:{}:2", recording.id).as_str()));
1530        assert!(records.iter().all(|record| {
1531            record
1532                .state
1533                .pointer("/ingressSource/metadata/pieceCount")
1534                .and_then(Value::as_u64)
1535                == Some(3)
1536                && record
1537                    .state
1538                    .pointer("/ingressSource/metadata/segmentationVersion")
1539                    .and_then(Value::as_u64)
1540                    == Some(SEGMENTATION_VERSION)
1541        }));
1542    }
1543
1544    #[tokio::test]
1545    async fn ambiguous_legacy_metadata_is_rejected_instead_of_remapping_identity() {
1546        let root = TestRoot::new();
1547        let history = history(root.path());
1548        let recording = completed_recording("Transcript");
1549        let id = audio_piece_id(recording.id, 0);
1550        history
1551            .enqueue_ingress(NewIngressSession {
1552                idempotency_id: id,
1553                started_at: recording.recorded_at.to_rfc3339(),
1554                source_session_type: "audio".into(),
1555                kind: SessionKind::AudioIngress,
1556                effective_context_tokens: 400,
1557                text: "Previously ingressed".into(),
1558                metadata: json!({
1559                    "kind":"audio-transcript",
1560                    "recordingId":recording.id.to_string(),
1561                    "sha256":recording.sha256,
1562                    "originalFilename":recording.original_filename,
1563                    "sizeBytes":recording.size_bytes,
1564                    "sourceCreatedAt":recording.recorded_at.to_rfc3339(),
1565                    "pieceIndex":0,
1566                    "pieceCount":1,
1567                }),
1568            })
1569            .await
1570            .unwrap();
1571
1572        let error = synchronize_recordings(&history, std::slice::from_ref(&recording), 400, 400)
1573            .await
1574            .unwrap_err();
1575        assert_eq!(error.kind(), ErrorKind::Conflict);
1576        assert_eq!(history.list().await.unwrap().len(), 1);
1577    }
1578
1579    #[tokio::test]
1580    async fn context_changes_preserve_authoritative_piece_text_counts_and_ids() {
1581        let root = TestRoot::new();
1582        let history = history(root.path());
1583        let recording = completed_recording("a".repeat(801));
1584
1585        synchronize_recordings(&history, std::slice::from_ref(&recording), 400, 400)
1586            .await
1587            .unwrap();
1588        let before_records = history.list().await.unwrap();
1589        let before = ingress_projection(&recording, &before_records, 400, 400)
1590            .unwrap()
1591            .pieces
1592            .into_iter()
1593            .map(|piece| (piece.piece_index, piece.piece_count, piece.transcript_text))
1594            .collect::<Vec<_>>();
1595        let before_ids = before_records
1596            .iter()
1597            .filter_map(ingress_source_id)
1598            .map(str::to_owned)
1599            .collect::<HashSet<_>>();
1600        assert_eq!(before.len(), 3);
1601
1602        synchronize_recordings(&history, std::slice::from_ref(&recording), 800, 800)
1603            .await
1604            .unwrap();
1605        let after_records = history.list().await.unwrap();
1606        let after = ingress_projection(&recording, &after_records, 800, 800)
1607            .unwrap()
1608            .pieces
1609            .into_iter()
1610            .map(|piece| (piece.piece_index, piece.piece_count, piece.transcript_text))
1611            .collect::<Vec<_>>();
1612        let after_ids = after_records
1613            .iter()
1614            .filter_map(ingress_source_id)
1615            .map(str::to_owned)
1616            .collect::<HashSet<_>>();
1617
1618        assert_eq!(after, before);
1619        assert_eq!(after_ids, before_ids);
1620        assert_eq!(after_records.len(), 3);
1621        assert!(after_records.iter().all(|record| {
1622            record
1623                .state
1624                .pointer("/ingressSource/metadata/effectiveContextTokens")
1625                .and_then(Value::as_u64)
1626                == Some(400)
1627        }));
1628    }
1629
1630    #[tokio::test]
1631    async fn partial_synchronization_uses_the_original_authoritative_segmentation() {
1632        let root = TestRoot::new();
1633        let history = history(root.path());
1634        let recording = completed_recording("a".repeat(801));
1635        let plan = new_segmentation_plan(&recording, 400, 400).unwrap();
1636        let first = &plan.specs[0];
1637        let first_text = format_ingress_piece(&recording, first).unwrap();
1638        history
1639            .enqueue_ingress(NewIngressSession {
1640                idempotency_id: first.id.clone(),
1641                started_at: recording.recorded_at.to_rfc3339(),
1642                source_session_type: "audio".into(),
1643                kind: SessionKind::AudioIngress,
1644                effective_context_tokens: plan.parameters.effective_context_tokens,
1645                text: first_text.clone(),
1646                metadata: audio_piece_metadata(&recording, first, &plan.parameters, &first_text),
1647            })
1648            .await
1649            .unwrap();
1650
1651        synchronize_recordings(&history, std::slice::from_ref(&recording), 800, 800)
1652            .await
1653            .unwrap();
1654
1655        let records = history.list().await.unwrap();
1656        let projection = ingress_projection(&recording, &records, 800, 800).unwrap();
1657        assert_eq!(projection.pieces.len(), 3);
1658        assert_eq!(
1659            projection
1660                .pieces
1661                .iter()
1662                .map(|piece| piece.transcript_text.clone())
1663                .collect::<Vec<_>>(),
1664            plan.specs
1665                .iter()
1666                .map(|spec| spec.text.clone())
1667                .collect::<Vec<_>>()
1668        );
1669        assert!(records.iter().all(|record| {
1670            record
1671                .state
1672                .pointer("/ingressSource/metadata/maximumPieceCharacters")
1673                .and_then(Value::as_u64)
1674                == Some(400)
1675                && record
1676                    .state
1677                    .pointer("/ingressSource/metadata/pieceCount")
1678                    .and_then(Value::as_u64)
1679                    == Some(3)
1680        }));
1681    }
1682
1683    #[tokio::test]
1684    async fn changed_transcript_conflicts_with_persisted_piece_identity() {
1685        let root = TestRoot::new();
1686        let history = history(root.path());
1687        let recording = completed_recording("a".repeat(801));
1688        synchronize_recordings(&history, std::slice::from_ref(&recording), 400, 400)
1689            .await
1690            .unwrap();
1691
1692        let mut changed = recording.clone();
1693        changed.state = RecordingState::Complete {
1694            transcript: format!("{}b", "a".repeat(800)),
1695        };
1696        let error = synchronize_recordings(&history, std::slice::from_ref(&changed), 400, 400)
1697            .await
1698            .unwrap_err();
1699
1700        assert_eq!(error.kind(), ErrorKind::Conflict);
1701        assert_eq!(history.list().await.unwrap().len(), 3);
1702    }
1703
1704    #[test]
1705    fn complete_recording_projection_preserves_pre_extraction_fields() {
1706        let recording = completed_recording("Transcript");
1707        let received_at = recording.received_at.to_rfc3339();
1708        let combined = Recording::from_status(recording, &IngressProjection::default());
1709
1710        assert_eq!(combined.received_at, received_at);
1711        assert_eq!(combined.updated_at, received_at);
1712        assert_eq!(combined.status, "ready_for_ingress");
1713        assert!(combined.transcription_status.is_none());
1714        assert_eq!(combined.attempt_count, 0);
1715        assert!(combined.next_attempt_at.is_none());
1716        assert!(combined.last_error.is_none());
1717        assert_eq!(combined.transcript_piece_count, 0);
1718    }
1719}