Skip to main content

kcode_speaker_v3_analysis/
lib.rs

1use serde::{Deserialize, Serialize};
2use std::{collections::BTreeSet, error::Error, fmt};
3
4pub use kcode_speaker_v3_llm_protocol::{
5    GEMINI_FEATURE_PROMPT_ONE, GEMINI_FEATURE_PROMPT_ONE_REVISION, GEMINI_FEATURE_PROMPT_REVISIONS,
6    GEMINI_FEATURE_PROMPT_THREE, GEMINI_FEATURE_PROMPT_THREE_REVISION, GEMINI_FEATURE_PROMPT_TWO,
7    GEMINI_FEATURE_PROMPT_TWO_REVISION, GEMINI_TRANSCRIPT_PROMPT,
8    GEMINI_TRANSCRIPT_PROMPT_REVISION, GPT_STRUCTURING_PROMPT, GPT_STRUCTURING_PROMPT_REVISION,
9};
10pub use kcode_speaker_v3_schema::{
11    FEATURE_NAMES, FEATURE_SCHEMA_REVISION, FeatureVector24, LocalSpeakerLabel,
12    MAX_AUDIO_DURATION_MS, OGG_MEDIA_TYPE, OggAudioMetadata, StructuredAnalysis, StructuredSpeaker,
13    ValidationError, VocalGenderPresentation,
14};
15
16#[cfg(any(feature = "providers", feature = "adapter-providers", test))]
17use kcode_speaker_v3_gemini_analysis::GeminiFeatureProgress;
18#[cfg(any(feature = "providers", feature = "adapter-providers", test))]
19use kcode_speaker_v3_llm_protocol::SpeakerFeatureEvidence;
20#[cfg(any(feature = "providers", feature = "adapter-providers", test))]
21use std::{future::Future, pin::Pin};
22
23const GEMINI_MODEL_ID: &str = "gemini-3.1-pro-preview";
24const TERRA_MODEL_ID: &str = "gpt-5.6-terra";
25
26#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
27pub struct GeminiCohort {
28    pub model_id: String,
29    pub transcript_prompt_revision: String,
30    pub feature_prompt_revisions: [String; 3],
31    pub feature_schema_revision: String,
32}
33
34impl GeminiCohort {
35    pub fn new(model_id: impl Into<String>) -> Self {
36        Self {
37            model_id: model_id.into(),
38            transcript_prompt_revision: GEMINI_TRANSCRIPT_PROMPT_REVISION.into(),
39            feature_prompt_revisions: GEMINI_FEATURE_PROMPT_REVISIONS.map(str::to_owned),
40            feature_schema_revision: FEATURE_SCHEMA_REVISION.into(),
41        }
42    }
43
44    pub fn validate(&self) -> Result<(), ValidationError> {
45        validate_text(&self.model_id, "gemini_model_id")?;
46        validate_text(
47            &self.transcript_prompt_revision,
48            "transcript_prompt_revision",
49        )?;
50        for revision in &self.feature_prompt_revisions {
51            validate_text(revision, "feature_prompt_revision")?;
52        }
53        validate_text(&self.feature_schema_revision, "feature_schema_revision")
54    }
55}
56
57#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
58pub struct StructurerProvenance {
59    pub model_id: String,
60    pub prompt_revision: String,
61}
62
63impl StructurerProvenance {
64    pub fn new(model_id: impl Into<String>) -> Self {
65        Self {
66            model_id: model_id.into(),
67            prompt_revision: GPT_STRUCTURING_PROMPT_REVISION.into(),
68        }
69    }
70
71    pub fn validate(&self) -> Result<(), ValidationError> {
72        validate_text(&self.model_id, "structurer_model_id")?;
73        validate_text(&self.prompt_revision, "structurer_prompt_revision")
74    }
75}
76
77#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
78pub struct AnalysisEnvelope {
79    pub audio: OggAudioMetadata,
80    pub analysis: StructuredAnalysis,
81    pub gemini: GeminiCohort,
82    pub structurer: StructurerProvenance,
83}
84
85impl AnalysisEnvelope {
86    pub fn validate(&self) -> Result<(), ValidationError> {
87        self.audio.validate()?;
88        self.analysis.validate()?;
89        self.gemini.validate()?;
90        self.structurer.validate()
91    }
92}
93
94#[derive(Debug, Clone, PartialEq, Eq)]
95pub enum AnalysisStage {
96    Transcript,
97    SpeakerLabels,
98    SpeakerFeatures,
99    Structuring,
100}
101
102#[derive(Debug, Clone, PartialEq, Eq)]
103pub enum AnalysisJob {
104    Transcript,
105    SpeakerLabels,
106    SpeakerFeature {
107        speaker: LocalSpeakerLabel,
108        packet: u8,
109    },
110    Structuring,
111}
112
113#[derive(Debug, Clone, PartialEq, Eq)]
114pub enum AnalysisProgress {
115    JobStarted { sequence: u64, job: AnalysisJob },
116    JobSucceeded { sequence: u64 },
117    JobFailed { sequence: u64, error: String },
118    StageCompleted { stage: AnalysisStage },
119}
120
121#[derive(Debug, Clone, PartialEq, Eq)]
122pub enum AnalysisError {
123    Input(String),
124    Progress(String),
125    GeminiTranscript(String),
126    TerraLabels(String),
127    GeminiCache(String),
128    GeminiFeature {
129        speaker: LocalSpeakerLabel,
130        packet: u8,
131        message: String,
132    },
133    TerraStructuring(String),
134    TranscriptMismatch,
135    SpeakerSetMismatch,
136}
137
138impl fmt::Display for AnalysisError {
139    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
140        match self {
141            Self::Input(message) => write!(formatter, "invalid input: {message}"),
142            Self::Progress(message) => write!(formatter, "progress reporting failed: {message}"),
143            Self::GeminiTranscript(message) => {
144                write!(formatter, "Gemini transcript failed: {message}")
145            }
146            Self::TerraLabels(message) => {
147                write!(
148                    formatter,
149                    "Terra speaker-label extraction failed: {message}"
150                )
151            }
152            Self::GeminiCache(message) => {
153                write!(formatter, "Gemini feature cache creation failed: {message}")
154            }
155            Self::GeminiFeature {
156                speaker,
157                packet,
158                message,
159            } => write!(
160                formatter,
161                "Gemini feature call failed for {speaker}, packet {packet}: {message}"
162            ),
163            Self::TerraStructuring(message) => {
164                write!(formatter, "Terra final structuring failed: {message}")
165            }
166            Self::TranscriptMismatch => {
167                formatter.write_str("Terra returned a different transcript")
168            }
169            Self::SpeakerSetMismatch => {
170                formatter.write_str("Terra returned a different speaker set")
171            }
172        }
173    }
174}
175
176impl Error for AnalysisError {}
177
178#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
179pub struct ExecutedAnalysis {
180    pub envelope: AnalysisEnvelope,
181    pub label_extractor: StructurerProvenance,
182}
183
184#[cfg(any(feature = "providers", feature = "adapter-providers"))]
185pub struct Analyzer {
186    operations: ProviderOperations,
187}
188
189#[cfg(any(feature = "providers", feature = "adapter-providers"))]
190impl Analyzer {
191    #[cfg(feature = "providers")]
192    pub fn new(
193        gemini: kcode_gemini_3_1_pro::Gemini31Pro,
194        terra: kcode_codex_terra::CodexTerra,
195    ) -> Self {
196        Self {
197            operations: ProviderOperations {
198                gemini: kcode_speaker_v3_gemini_analysis::GeminiAnalysis::new(gemini),
199                terra: kcode_speaker_v3_terra_analysis::TerraAnalysis::new(terra),
200            },
201        }
202    }
203
204    #[cfg(feature = "adapter-providers")]
205    pub fn from_codex_adapter(
206        gemini: kcode_gemini_3_1_pro::Gemini31Pro,
207        adapter: kcode_k1_codex_adapter::Adapter,
208    ) -> Self {
209        Self {
210            operations: ProviderOperations {
211                gemini: kcode_speaker_v3_gemini_analysis::GeminiAnalysis::new(gemini),
212                terra: kcode_speaker_v3_terra_analysis::TerraAnalysis::from_codex_adapter(adapter),
213            },
214        }
215    }
216
217    pub async fn analyze_ogg_with_progress<F>(
218        &self,
219        bytes: &[u8],
220        report: F,
221    ) -> Result<ExecutedAnalysis, AnalysisError>
222    where
223        F: FnMut(AnalysisProgress) -> Result<(), String>,
224    {
225        execute_strict(&self.operations, bytes, report).await
226    }
227
228    pub async fn analyze_ogg(
229        &self,
230        bytes: &[u8],
231        duration_ms: u64,
232        filename: Option<String>,
233    ) -> Result<ExecutedAnalysis, AnalysisError> {
234        execute_legacy(&self.operations, bytes, duration_ms, filename).await
235    }
236}
237
238fn validate_text(value: &str, field: &'static str) -> Result<(), ValidationError> {
239    (!value.trim().is_empty())
240        .then_some(())
241        .ok_or(ValidationError::Blank(field))
242}
243
244#[cfg(any(feature = "providers", feature = "adapter-providers", test))]
245type AnalysisFuture<'a, T> = Pin<Box<dyn Future<Output = T> + 'a>>;
246
247#[cfg(any(feature = "providers", feature = "adapter-providers", test))]
248trait AnalysisOperations: Sync {
249    fn transcript<'a>(
250        &'a self,
251        audio: &'a [u8],
252    ) -> AnalysisFuture<'a, Result<String, AnalysisError>>;
253
254    fn speaker_labels<'a>(
255        &'a self,
256        transcript: &'a str,
257    ) -> AnalysisFuture<'a, Result<Vec<LocalSpeakerLabel>, AnalysisError>>;
258
259    fn feature_evidence_with_progress<'a>(
260        &'a self,
261        audio: &'a [u8],
262        transcript: &'a str,
263        labels: &'a [LocalSpeakerLabel],
264        report: &'a mut dyn FnMut(GeminiFeatureProgress) -> Result<(), String>,
265    ) -> AnalysisFuture<'a, Result<Vec<SpeakerFeatureEvidence>, AnalysisError>>;
266
267    fn structured_analysis<'a>(
268        &'a self,
269        transcript: &'a str,
270        evidence: Vec<SpeakerFeatureEvidence>,
271    ) -> AnalysisFuture<'a, Result<StructuredAnalysis, AnalysisError>>;
272}
273
274#[cfg(any(feature = "providers", feature = "adapter-providers", test))]
275struct ProviderOperations {
276    gemini: kcode_speaker_v3_gemini_analysis::GeminiAnalysis,
277    terra: kcode_speaker_v3_terra_analysis::TerraAnalysis,
278}
279
280#[cfg(any(feature = "providers", feature = "adapter-providers", test))]
281impl AnalysisOperations for ProviderOperations {
282    fn transcript<'a>(
283        &'a self,
284        audio: &'a [u8],
285    ) -> AnalysisFuture<'a, Result<String, AnalysisError>> {
286        Box::pin(async move {
287            self.gemini.transcript(audio).await.map_err(|error| match error {
288                kcode_speaker_v3_gemini_analysis::GeminiTranscriptError::Provider(message)
289                | kcode_speaker_v3_gemini_analysis::GeminiTranscriptError::Protocol(message) => {
290                    AnalysisError::GeminiTranscript(message)
291                }
292            })
293        })
294    }
295
296    fn speaker_labels<'a>(
297        &'a self,
298        transcript: &'a str,
299    ) -> AnalysisFuture<'a, Result<Vec<LocalSpeakerLabel>, AnalysisError>> {
300        Box::pin(async move {
301            self.terra
302                .speaker_labels(transcript)
303                .await
304                .map_err(|error| match error {
305                    kcode_speaker_v3_terra_analysis::TerraAnalysisError::Protocol(message)
306                    | kcode_speaker_v3_terra_analysis::TerraAnalysisError::Provider(message) => {
307                        AnalysisError::TerraLabels(message)
308                    }
309                })
310        })
311    }
312
313    fn feature_evidence_with_progress<'a>(
314        &'a self,
315        audio: &'a [u8],
316        transcript: &'a str,
317        labels: &'a [LocalSpeakerLabel],
318        report: &'a mut dyn FnMut(GeminiFeatureProgress) -> Result<(), String>,
319    ) -> AnalysisFuture<'a, Result<Vec<SpeakerFeatureEvidence>, AnalysisError>> {
320        Box::pin(async move {
321            self.gemini
322                .feature_evidence_with_progress(audio, transcript, labels, report)
323                .await
324                .map_err(|error| match error {
325                    kcode_speaker_v3_gemini_analysis::GeminiFeatureError::Cache(message) => {
326                        AnalysisError::GeminiCache(message)
327                    }
328                    kcode_speaker_v3_gemini_analysis::GeminiFeatureError::Progress(message) => {
329                        AnalysisError::Progress(message)
330                    }
331                    kcode_speaker_v3_gemini_analysis::GeminiFeatureError::Feature {
332                        speaker,
333                        packet,
334                        message,
335                    } => AnalysisError::GeminiFeature {
336                        speaker,
337                        packet,
338                        message,
339                    },
340                })
341        })
342    }
343
344    fn structured_analysis<'a>(
345        &'a self,
346        transcript: &'a str,
347        evidence: Vec<SpeakerFeatureEvidence>,
348    ) -> AnalysisFuture<'a, Result<StructuredAnalysis, AnalysisError>> {
349        Box::pin(async move {
350            self.terra
351                .structured_analysis(transcript, evidence)
352                .await
353                .map_err(|error| match error {
354                    kcode_speaker_v3_terra_analysis::TerraAnalysisError::Protocol(message)
355                    | kcode_speaker_v3_terra_analysis::TerraAnalysisError::Provider(message) => {
356                        AnalysisError::TerraStructuring(message)
357                    }
358                })
359        })
360    }
361}
362
363#[cfg(any(feature = "providers", feature = "adapter-providers", test))]
364async fn execute_strict<O, F>(
365    operations: &O,
366    bytes: &[u8],
367    report: F,
368) -> Result<ExecutedAnalysis, AnalysisError>
369where
370    O: AnalysisOperations,
371    F: FnMut(AnalysisProgress) -> Result<(), String>,
372{
373    let audio = OggAudioMetadata::from_ogg_bytes(bytes)
374        .map_err(|error| AnalysisError::Input(error.to_string()))?;
375    execute_admitted(operations, bytes, audio, report).await
376}
377
378#[cfg(any(feature = "providers", feature = "adapter-providers", test))]
379async fn execute_legacy<O: AnalysisOperations>(
380    operations: &O,
381    bytes: &[u8],
382    duration_ms: u64,
383    filename: Option<String>,
384) -> Result<ExecutedAnalysis, AnalysisError> {
385    let audio = OggAudioMetadata::from_bytes(bytes, duration_ms, filename)
386        .map_err(|error| AnalysisError::Input(error.to_string()))?;
387    execute_admitted(operations, bytes, audio, |_| Ok(())).await
388}
389
390#[cfg(any(feature = "providers", feature = "adapter-providers", test))]
391async fn execute_admitted<O, F>(
392    operations: &O,
393    bytes: &[u8],
394    audio: OggAudioMetadata,
395    mut report: F,
396) -> Result<ExecutedAnalysis, AnalysisError>
397where
398    O: AnalysisOperations,
399    F: FnMut(AnalysisProgress) -> Result<(), String>,
400{
401    report_progress(
402        &mut report,
403        AnalysisProgress::JobStarted {
404            sequence: 1,
405            job: AnalysisJob::Transcript,
406        },
407    )?;
408    let transcript = match operations.transcript(bytes).await {
409        Ok(transcript) => transcript,
410        Err(error) => {
411            report_leaf_failure(&mut report, 1, &error)?;
412            return Err(error);
413        }
414    };
415    report_progress(&mut report, AnalysisProgress::JobSucceeded { sequence: 1 })?;
416    report_progress(
417        &mut report,
418        AnalysisProgress::StageCompleted {
419            stage: AnalysisStage::Transcript,
420        },
421    )?;
422
423    report_progress(
424        &mut report,
425        AnalysisProgress::JobStarted {
426            sequence: 2,
427            job: AnalysisJob::SpeakerLabels,
428        },
429    )?;
430    let labels = match operations.speaker_labels(&transcript).await {
431        Ok(labels) => labels,
432        Err(error) => {
433            report_leaf_failure(&mut report, 2, &error)?;
434            return Err(error);
435        }
436    };
437    report_progress(&mut report, AnalysisProgress::JobSucceeded { sequence: 2 })?;
438    report_progress(
439        &mut report,
440        AnalysisProgress::StageCompleted {
441            stage: AnalysisStage::SpeakerLabels,
442        },
443    )?;
444
445    let structuring_sequence = structuring_sequence(labels.len())?;
446    let evidence = {
447        let mut feature_report = |progress| {
448            let progress = map_feature_progress(&labels, progress)?;
449            report(progress)
450        };
451        operations
452            .feature_evidence_with_progress(bytes, &transcript, &labels, &mut feature_report)
453            .await?
454    };
455    report_progress(
456        &mut report,
457        AnalysisProgress::StageCompleted {
458            stage: AnalysisStage::SpeakerFeatures,
459        },
460    )?;
461
462    report_progress(
463        &mut report,
464        AnalysisProgress::JobStarted {
465            sequence: structuring_sequence,
466            job: AnalysisJob::Structuring,
467        },
468    )?;
469    let analysis = match operations.structured_analysis(&transcript, evidence).await {
470        Ok(analysis) => analysis,
471        Err(error) => {
472            report_leaf_failure(&mut report, structuring_sequence, &error)?;
473            return Err(error);
474        }
475    };
476    report_progress(
477        &mut report,
478        AnalysisProgress::JobSucceeded {
479            sequence: structuring_sequence,
480        },
481    )?;
482
483    if analysis.transcript != transcript {
484        return Err(AnalysisError::TranscriptMismatch);
485    }
486
487    let expected_speakers = labels.iter().copied().collect::<BTreeSet<_>>();
488    let returned_speakers = analysis
489        .speakers
490        .iter()
491        .map(|speaker| speaker.speaker)
492        .collect::<BTreeSet<_>>();
493    if expected_speakers != returned_speakers {
494        return Err(AnalysisError::SpeakerSetMismatch);
495    }
496
497    let envelope = AnalysisEnvelope {
498        audio,
499        analysis,
500        gemini: GeminiCohort::new(GEMINI_MODEL_ID),
501        structurer: StructurerProvenance::new(TERRA_MODEL_ID),
502    };
503    envelope
504        .validate()
505        .map_err(|error| AnalysisError::TerraStructuring(error.to_string()))?;
506
507    let label_extractor = StructurerProvenance {
508        model_id: TERRA_MODEL_ID.into(),
509        prompt_revision: kcode_speaker_v3_llm_protocol::TERRA_SPEAKER_LABELS_PROMPT_REVISION.into(),
510    };
511    label_extractor
512        .validate()
513        .map_err(|error| AnalysisError::TerraLabels(error.to_string()))?;
514
515    let executed = ExecutedAnalysis {
516        envelope,
517        label_extractor,
518    };
519    report_progress(
520        &mut report,
521        AnalysisProgress::StageCompleted {
522            stage: AnalysisStage::Structuring,
523        },
524    )?;
525    Ok(executed)
526}
527
528#[cfg(any(feature = "providers", feature = "adapter-providers", test))]
529fn report_progress<F>(report: &mut F, progress: AnalysisProgress) -> Result<(), AnalysisError>
530where
531    F: FnMut(AnalysisProgress) -> Result<(), String>,
532{
533    report(progress).map_err(AnalysisError::Progress)
534}
535
536#[cfg(any(feature = "providers", feature = "adapter-providers", test))]
537fn report_leaf_failure<F>(
538    report: &mut F,
539    sequence: u64,
540    error: &AnalysisError,
541) -> Result<(), AnalysisError>
542where
543    F: FnMut(AnalysisProgress) -> Result<(), String>,
544{
545    report_progress(
546        report,
547        AnalysisProgress::JobFailed {
548            sequence,
549            error: error.to_string(),
550        },
551    )
552}
553
554#[cfg(any(feature = "providers", feature = "adapter-providers", test))]
555fn structuring_sequence(label_count: usize) -> Result<u64, AnalysisError> {
556    u64::try_from(label_count)
557        .ok()
558        .and_then(|count| count.checked_mul(3))
559        .and_then(|count| count.checked_add(3))
560        .ok_or_else(sequence_error)
561}
562
563#[cfg(any(feature = "providers", feature = "adapter-providers", test))]
564fn feature_sequence(
565    labels: &[LocalSpeakerLabel],
566    speaker: LocalSpeakerLabel,
567    packet: u8,
568) -> Result<u64, String> {
569    if !(1..=3).contains(&packet) {
570        return Err(format!(
571            "Gemini reported invalid feature packet {packet} for {speaker}"
572        ));
573    }
574    let label_index = labels
575        .iter()
576        .position(|candidate| *candidate == speaker)
577        .ok_or_else(|| format!("Gemini reported an unknown feature speaker {speaker}"))?;
578    u64::try_from(label_index)
579        .ok()
580        .and_then(|index| index.checked_mul(3))
581        .and_then(|index| index.checked_add(u64::from(packet)))
582        .and_then(|index| index.checked_add(2))
583        .ok_or_else(|| sequence_error().to_string())
584}
585
586#[cfg(any(feature = "providers", feature = "adapter-providers", test))]
587fn sequence_error() -> AnalysisError {
588    AnalysisError::Progress("analysis job sequence overflow".into())
589}
590
591#[cfg(any(feature = "providers", feature = "adapter-providers", test))]
592fn map_feature_progress(
593    labels: &[LocalSpeakerLabel],
594    progress: GeminiFeatureProgress,
595) -> Result<AnalysisProgress, String> {
596    match progress {
597        GeminiFeatureProgress::Started { speaker, packet } => Ok(AnalysisProgress::JobStarted {
598            sequence: feature_sequence(labels, speaker, packet)?,
599            job: AnalysisJob::SpeakerFeature { speaker, packet },
600        }),
601        GeminiFeatureProgress::Succeeded { speaker, packet } => {
602            Ok(AnalysisProgress::JobSucceeded {
603                sequence: feature_sequence(labels, speaker, packet)?,
604            })
605        }
606        GeminiFeatureProgress::Failed {
607            speaker,
608            packet,
609            error,
610        } => {
611            let displayed_error = AnalysisError::GeminiFeature {
612                speaker,
613                packet,
614                message: error,
615            }
616            .to_string();
617            Ok(AnalysisProgress::JobFailed {
618                sequence: feature_sequence(labels, speaker, packet)?,
619                error: displayed_error,
620            })
621        }
622    }
623}
624
625#[cfg(test)]
626mod tests {
627    use super::*;
628    use futures::{executor::block_on, future::poll_fn, join};
629    use std::{
630        sync::{
631            Arc, Mutex,
632            atomic::{AtomicBool, AtomicUsize, Ordering},
633        },
634        task::Poll,
635        time::Instant,
636    };
637
638    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
639    enum FailureStage {
640        Transcript,
641        Labels,
642        Cache,
643        Features,
644        Final,
645    }
646
647    #[derive(Clone)]
648    struct FakeConfig {
649        transcript: String,
650        labels: Vec<LocalSpeakerLabel>,
651        analysis: StructuredAnalysis,
652        failure: Option<FailureStage>,
653        feature_terminal_order: Option<Vec<(LocalSpeakerLabel, u8)>>,
654        feature_failures: Vec<(LocalSpeakerLabel, u8, String)>,
655        wait_for: Option<Arc<AtomicBool>>,
656        mark_complete: Option<Arc<AtomicBool>>,
657    }
658
659    struct FakeState {
660        config: FakeConfig,
661        transcript_calls: AtomicUsize,
662        label_calls: AtomicUsize,
663        feature_calls: AtomicUsize,
664        final_calls: AtomicUsize,
665        calls: Mutex<Vec<&'static str>>,
666    }
667
668    #[derive(Clone)]
669    struct FakeOperations {
670        state: Arc<FakeState>,
671    }
672
673    impl FakeOperations {
674        fn successful(speaker_count: u32) -> Self {
675            let transcript = "[high] Speaker 1: exact transcript".to_owned();
676            Self::from_config(FakeConfig {
677                labels: (1..=speaker_count).map(label).collect(),
678                analysis: structured_analysis(&transcript, speaker_count),
679                transcript,
680                failure: None,
681                feature_terminal_order: None,
682                feature_failures: Vec::new(),
683                wait_for: None,
684                mark_complete: None,
685            })
686        }
687
688        fn from_config(config: FakeConfig) -> Self {
689            Self {
690                state: Arc::new(FakeState {
691                    config,
692                    transcript_calls: AtomicUsize::new(0),
693                    label_calls: AtomicUsize::new(0),
694                    feature_calls: AtomicUsize::new(0),
695                    final_calls: AtomicUsize::new(0),
696                    calls: Mutex::new(Vec::new()),
697                }),
698            }
699        }
700
701        fn with_config(&self, update: impl FnOnce(&mut FakeConfig)) -> Self {
702            let mut config = self.state.config.clone();
703            update(&mut config);
704            Self::from_config(config)
705        }
706
707        fn feature_failure(&self, speaker: LocalSpeakerLabel, packet: u8) -> Option<String> {
708            if self.state.config.failure == Some(FailureStage::Features)
709                && speaker == label(1)
710                && packet == 2
711            {
712                return Some("features".into());
713            }
714            self.state
715                .config
716                .feature_failures
717                .iter()
718                .find(|(candidate, candidate_packet, _)| {
719                    *candidate == speaker && *candidate_packet == packet
720                })
721                .map(|(_, _, message)| message.clone())
722        }
723    }
724
725    impl AnalysisOperations for FakeOperations {
726        fn transcript<'a>(
727            &'a self,
728            _audio: &'a [u8],
729        ) -> AnalysisFuture<'a, Result<String, AnalysisError>> {
730            Box::pin(async move {
731                self.state.transcript_calls.fetch_add(1, Ordering::SeqCst);
732                self.state.calls.lock().unwrap().push("transcript");
733                if let Some(wait_for) = &self.state.config.wait_for {
734                    poll_fn(|context| {
735                        if wait_for.load(Ordering::SeqCst) {
736                            Poll::Ready(())
737                        } else {
738                            context.waker().wake_by_ref();
739                            Poll::Pending
740                        }
741                    })
742                    .await;
743                }
744                if self.state.config.failure == Some(FailureStage::Transcript) {
745                    return Err(AnalysisError::GeminiTranscript("transcript".into()));
746                }
747                Ok(self.state.config.transcript.clone())
748            })
749        }
750
751        fn speaker_labels<'a>(
752            &'a self,
753            _transcript: &'a str,
754        ) -> AnalysisFuture<'a, Result<Vec<LocalSpeakerLabel>, AnalysisError>> {
755            Box::pin(async move {
756                self.state.label_calls.fetch_add(1, Ordering::SeqCst);
757                self.state.calls.lock().unwrap().push("labels");
758                if self.state.config.failure == Some(FailureStage::Labels) {
759                    return Err(AnalysisError::TerraLabels("labels".into()));
760                }
761                Ok(self.state.config.labels.clone())
762            })
763        }
764
765        fn feature_evidence_with_progress<'a>(
766            &'a self,
767            _audio: &'a [u8],
768            _transcript: &'a str,
769            labels: &'a [LocalSpeakerLabel],
770            report: &'a mut dyn FnMut(GeminiFeatureProgress) -> Result<(), String>,
771        ) -> AnalysisFuture<'a, Result<Vec<SpeakerFeatureEvidence>, AnalysisError>> {
772            Box::pin(async move {
773                self.state.feature_calls.fetch_add(1, Ordering::SeqCst);
774                self.state.calls.lock().unwrap().push("features");
775                if self.state.config.failure == Some(FailureStage::Cache) {
776                    return Err(AnalysisError::GeminiCache("cache".into()));
777                }
778
779                let jobs = labels
780                    .iter()
781                    .copied()
782                    .flat_map(|speaker| (1..=3).map(move |packet| (speaker, packet)))
783                    .collect::<Vec<_>>();
784                for &(speaker, packet) in &jobs {
785                    report(GeminiFeatureProgress::Started { speaker, packet })
786                        .map_err(AnalysisError::Progress)?;
787                }
788
789                let terminal_order = self
790                    .state
791                    .config
792                    .feature_terminal_order
793                    .clone()
794                    .unwrap_or_else(|| jobs.clone());
795                for (speaker, packet) in terminal_order {
796                    if let Some(error) = self.feature_failure(speaker, packet) {
797                        report(GeminiFeatureProgress::Failed {
798                            speaker,
799                            packet,
800                            error,
801                        })
802                        .map_err(AnalysisError::Progress)?;
803                    } else {
804                        report(GeminiFeatureProgress::Succeeded { speaker, packet })
805                            .map_err(AnalysisError::Progress)?;
806                    }
807                }
808
809                if let Some((speaker, packet, message)) =
810                    jobs.iter().find_map(|&(speaker, packet)| {
811                        self.feature_failure(speaker, packet)
812                            .map(|message| (speaker, packet, message))
813                    })
814                {
815                    return Err(AnalysisError::GeminiFeature {
816                        speaker,
817                        packet,
818                        message,
819                    });
820                }
821
822                labels
823                    .iter()
824                    .copied()
825                    .map(|speaker| {
826                        SpeakerFeatureEvidence::new(
827                            speaker,
828                            format!("{speaker} packet 1"),
829                            format!("{speaker} packet 2"),
830                            format!("{speaker} packet 3"),
831                        )
832                        .map_err(|error| AnalysisError::GeminiFeature {
833                            speaker,
834                            packet: 1,
835                            message: error.to_string(),
836                        })
837                    })
838                    .collect()
839            })
840        }
841
842        fn structured_analysis<'a>(
843            &'a self,
844            _transcript: &'a str,
845            _evidence: Vec<SpeakerFeatureEvidence>,
846        ) -> AnalysisFuture<'a, Result<StructuredAnalysis, AnalysisError>> {
847            Box::pin(async move {
848                self.state.final_calls.fetch_add(1, Ordering::SeqCst);
849                self.state.calls.lock().unwrap().push("final");
850                if self.state.config.failure == Some(FailureStage::Final) {
851                    return Err(AnalysisError::TerraStructuring("final".into()));
852                }
853                if let Some(mark_complete) = &self.state.config.mark_complete {
854                    mark_complete.store(true, Ordering::SeqCst);
855                }
856                Ok(self.state.config.analysis.clone())
857            })
858        }
859    }
860
861    fn label(number: u32) -> LocalSpeakerLabel {
862        LocalSpeakerLabel::new(number).unwrap()
863    }
864
865    fn structured_analysis(transcript: &str, speaker_count: u32) -> StructuredAnalysis {
866        StructuredAnalysis {
867            transcript: transcript.into(),
868            speakers: (1..=speaker_count)
869                .map(|number| StructuredSpeaker {
870                    speaker: label(number),
871                    language: "English".into(),
872                    features: FeatureVector24::default(),
873                    features_usable_for_training: false,
874                })
875                .collect(),
876        }
877    }
878
879    fn legacy_ogg() -> Vec<u8> {
880        let mut bytes = vec![0; 28];
881        bytes[..4].copy_from_slice(b"OggS");
882        bytes[4] = 0;
883        bytes[26] = 1;
884        bytes[27] = 0;
885        bytes
886    }
887
888    fn strict_ogg(samples: u64) -> Vec<u8> {
889        let pre_skip = 312_u16;
890        let mut head = b"OpusHead".to_vec();
891        head.push(1);
892        head.push(1);
893        head.extend_from_slice(&pre_skip.to_le_bytes());
894        head.extend_from_slice(&48_000_u32.to_le_bytes());
895        head.extend_from_slice(&0_i16.to_le_bytes());
896        head.push(0);
897
898        let mut tags = b"OpusTags".to_vec();
899        tags.extend_from_slice(&0_u32.to_le_bytes());
900        tags.extend_from_slice(&0_u32.to_le_bytes());
901
902        let mut bytes = ogg_page(0x02, 0, 0, &head);
903        bytes.extend_from_slice(&ogg_page(0, 0, 1, &tags));
904        bytes.extend_from_slice(&ogg_page(
905            0x04,
906            u64::from(pre_skip) + samples,
907            2,
908            &[0xf8, 0xff, 0xfe],
909        ));
910        bytes
911    }
912
913    fn ogg_page(header_type: u8, granule: u64, sequence: u32, payload: &[u8]) -> Vec<u8> {
914        let payload_length = u8::try_from(payload.len()).unwrap();
915        let mut page = Vec::with_capacity(28 + payload.len());
916        page.extend_from_slice(b"OggS");
917        page.push(0);
918        page.push(header_type);
919        page.extend_from_slice(&granule.to_le_bytes());
920        page.extend_from_slice(&0x534b_5633_u32.to_le_bytes());
921        page.extend_from_slice(&sequence.to_le_bytes());
922        page.extend_from_slice(&0_u32.to_le_bytes());
923        page.push(1);
924        page.push(payload_length);
925        page.extend_from_slice(payload);
926        let checksum = ogg_crc(&page);
927        page[22..26].copy_from_slice(&checksum.to_le_bytes());
928        page
929    }
930
931    fn ogg_crc(bytes: &[u8]) -> u32 {
932        let mut crc = 0_u32;
933        for &byte in bytes {
934            crc ^= u32::from(byte) << 24;
935            for _ in 0..8 {
936                crc = if crc & 0x8000_0000 == 0 {
937                    crc << 1
938                } else {
939                    (crc << 1) ^ 0x04c1_1db7
940                };
941            }
942        }
943        crc
944    }
945
946    fn run_with_progress(
947        operations: &FakeOperations,
948    ) -> (
949        Result<ExecutedAnalysis, AnalysisError>,
950        Vec<AnalysisProgress>,
951    ) {
952        let audio = strict_ogg(48);
953        let mut events = Vec::new();
954        let result = block_on(execute_strict(operations, &audio, |event| {
955            events.push(event);
956            Ok(())
957        }));
958        (result, events)
959    }
960
961    #[test]
962    fn strict_admission_precedes_reports_and_operations() {
963        for bytes in [b"bad".to_vec(), strict_ogg(7_200_001)] {
964            let operations = FakeOperations::successful(1);
965            let reports = AtomicUsize::new(0);
966            assert!(matches!(
967                block_on(execute_strict(&operations, &bytes, |_| {
968                    reports.fetch_add(1, Ordering::SeqCst);
969                    Ok(())
970                })),
971                Err(AnalysisError::Input(_))
972            ));
973            assert_eq!(reports.load(Ordering::SeqCst), 0);
974            assert!(operations.state.calls.lock().unwrap().is_empty());
975        }
976    }
977
978    #[test]
979    fn zero_label_sequences_are_exact() {
980        let operations = FakeOperations::successful(0);
981        let (result, events) = run_with_progress(&operations);
982        result.unwrap();
983        assert_eq!(
984            events,
985            vec![
986                AnalysisProgress::JobStarted {
987                    sequence: 1,
988                    job: AnalysisJob::Transcript,
989                },
990                AnalysisProgress::JobSucceeded { sequence: 1 },
991                AnalysisProgress::StageCompleted {
992                    stage: AnalysisStage::Transcript,
993                },
994                AnalysisProgress::JobStarted {
995                    sequence: 2,
996                    job: AnalysisJob::SpeakerLabels,
997                },
998                AnalysisProgress::JobSucceeded { sequence: 2 },
999                AnalysisProgress::StageCompleted {
1000                    stage: AnalysisStage::SpeakerLabels,
1001                },
1002                AnalysisProgress::StageCompleted {
1003                    stage: AnalysisStage::SpeakerFeatures,
1004                },
1005                AnalysisProgress::JobStarted {
1006                    sequence: 3,
1007                    job: AnalysisJob::Structuring,
1008                },
1009                AnalysisProgress::JobSucceeded { sequence: 3 },
1010                AnalysisProgress::StageCompleted {
1011                    stage: AnalysisStage::Structuring,
1012                },
1013            ]
1014        );
1015    }
1016
1017    #[test]
1018    fn multiple_label_sequences_are_exact() {
1019        let operations = FakeOperations::successful(2);
1020        let (result, events) = run_with_progress(&operations);
1021        result.unwrap();
1022
1023        let mut expected = vec![
1024            AnalysisProgress::JobStarted {
1025                sequence: 1,
1026                job: AnalysisJob::Transcript,
1027            },
1028            AnalysisProgress::JobSucceeded { sequence: 1 },
1029            AnalysisProgress::StageCompleted {
1030                stage: AnalysisStage::Transcript,
1031            },
1032            AnalysisProgress::JobStarted {
1033                sequence: 2,
1034                job: AnalysisJob::SpeakerLabels,
1035            },
1036            AnalysisProgress::JobSucceeded { sequence: 2 },
1037            AnalysisProgress::StageCompleted {
1038                stage: AnalysisStage::SpeakerLabels,
1039            },
1040        ];
1041        for (sequence, speaker, packet) in [
1042            (3, label(1), 1),
1043            (4, label(1), 2),
1044            (5, label(1), 3),
1045            (6, label(2), 1),
1046            (7, label(2), 2),
1047            (8, label(2), 3),
1048        ] {
1049            expected.push(AnalysisProgress::JobStarted {
1050                sequence,
1051                job: AnalysisJob::SpeakerFeature { speaker, packet },
1052            });
1053        }
1054        for sequence in 3..=8 {
1055            expected.push(AnalysisProgress::JobSucceeded { sequence });
1056        }
1057        expected.extend([
1058            AnalysisProgress::StageCompleted {
1059                stage: AnalysisStage::SpeakerFeatures,
1060            },
1061            AnalysisProgress::JobStarted {
1062                sequence: 9,
1063                job: AnalysisJob::Structuring,
1064            },
1065            AnalysisProgress::JobSucceeded { sequence: 9 },
1066            AnalysisProgress::StageCompleted {
1067                stage: AnalysisStage::Structuring,
1068            },
1069        ]);
1070        assert_eq!(events, expected);
1071    }
1072
1073    #[test]
1074    fn feature_terminals_preserve_actual_completion_order() {
1075        let operations = FakeOperations::successful(2).with_config(|config| {
1076            config.feature_terminal_order = Some(vec![
1077                (label(2), 2),
1078                (label(1), 3),
1079                (label(2), 1),
1080                (label(1), 1),
1081                (label(2), 3),
1082                (label(1), 2),
1083            ]);
1084        });
1085        let (result, events) = run_with_progress(&operations);
1086        result.unwrap();
1087        let terminals = events
1088            .iter()
1089            .filter_map(|event| match event {
1090                AnalysisProgress::JobSucceeded { sequence } if (3..9).contains(sequence) => {
1091                    Some(*sequence)
1092                }
1093                _ => None,
1094            })
1095            .collect::<Vec<_>>();
1096        assert_eq!(terminals, [7, 5, 6, 3, 8, 4]);
1097    }
1098
1099    #[test]
1100    fn multiple_feature_failures_are_all_reported_without_duplicate() {
1101        let operations = FakeOperations::successful(2).with_config(|config| {
1102            config.feature_terminal_order = Some(vec![
1103                (label(2), 1),
1104                (label(1), 2),
1105                (label(1), 1),
1106                (label(1), 3),
1107                (label(2), 2),
1108                (label(2), 3),
1109            ]);
1110            config.feature_failures = vec![
1111                (label(1), 2, "first deterministic failure".into()),
1112                (label(2), 1, "first completed failure".into()),
1113            ];
1114        });
1115        let (result, events) = run_with_progress(&operations);
1116        assert_eq!(
1117            result,
1118            Err(AnalysisError::GeminiFeature {
1119                speaker: label(1),
1120                packet: 2,
1121                message: "first deterministic failure".into(),
1122            })
1123        );
1124        let failures = events
1125            .iter()
1126            .filter_map(|event| match event {
1127                AnalysisProgress::JobFailed { sequence, error } => Some((*sequence, error.clone())),
1128                _ => None,
1129            })
1130            .collect::<Vec<_>>();
1131        assert_eq!(
1132            failures,
1133            vec![
1134                (
1135                    6,
1136                    AnalysisError::GeminiFeature {
1137                        speaker: label(2),
1138                        packet: 1,
1139                        message: "first completed failure".into(),
1140                    }
1141                    .to_string(),
1142                ),
1143                (
1144                    4,
1145                    AnalysisError::GeminiFeature {
1146                        speaker: label(1),
1147                        packet: 2,
1148                        message: "first deterministic failure".into(),
1149                    }
1150                    .to_string(),
1151                ),
1152            ]
1153        );
1154        assert!(!events.contains(&AnalysisProgress::StageCompleted {
1155            stage: AnalysisStage::SpeakerFeatures,
1156        }));
1157        assert_eq!(
1158            *operations.state.calls.lock().unwrap(),
1159            ["transcript", "labels", "features"]
1160        );
1161    }
1162
1163    #[test]
1164    fn reporter_failure_supersedes_and_stops_orchestration() {
1165        let operations = FakeOperations::successful(1);
1166        let audio = strict_ogg(48);
1167        let mut events = Vec::new();
1168        let result = block_on(execute_strict(&operations, &audio, |event| {
1169            events.push(event.clone());
1170            if event
1171                == (AnalysisProgress::JobStarted {
1172                    sequence: 3,
1173                    job: AnalysisJob::SpeakerFeature {
1174                        speaker: label(1),
1175                        packet: 1,
1176                    },
1177                })
1178            {
1179                Err("reporter closed".into())
1180            } else {
1181                Ok(())
1182            }
1183        }));
1184        assert_eq!(
1185            result,
1186            Err(AnalysisError::Progress("reporter closed".into()))
1187        );
1188        assert_eq!(
1189            *operations.state.calls.lock().unwrap(),
1190            ["transcript", "labels", "features"]
1191        );
1192        assert_eq!(
1193            events.last(),
1194            Some(&AnalysisProgress::JobStarted {
1195                sequence: 3,
1196                job: AnalysisJob::SpeakerFeature {
1197                    speaker: label(1),
1198                    packet: 1,
1199                },
1200            })
1201        );
1202    }
1203
1204    #[test]
1205    fn provider_stages_fail_without_retry_and_leaf_failures_are_exact() {
1206        for (stage, expected_error, expected_calls) in [
1207            (
1208                FailureStage::Transcript,
1209                AnalysisError::GeminiTranscript("transcript".into()),
1210                vec!["transcript"],
1211            ),
1212            (
1213                FailureStage::Labels,
1214                AnalysisError::TerraLabels("labels".into()),
1215                vec!["transcript", "labels"],
1216            ),
1217            (
1218                FailureStage::Cache,
1219                AnalysisError::GeminiCache("cache".into()),
1220                vec!["transcript", "labels", "features"],
1221            ),
1222            (
1223                FailureStage::Features,
1224                AnalysisError::GeminiFeature {
1225                    speaker: label(1),
1226                    packet: 2,
1227                    message: "features".into(),
1228                },
1229                vec!["transcript", "labels", "features"],
1230            ),
1231            (
1232                FailureStage::Final,
1233                AnalysisError::TerraStructuring("final".into()),
1234                vec!["transcript", "labels", "features", "final"],
1235            ),
1236        ] {
1237            let operations =
1238                FakeOperations::successful(1).with_config(|config| config.failure = Some(stage));
1239            let (result, events) = run_with_progress(&operations);
1240            assert_eq!(result, Err(expected_error.clone()));
1241            assert_eq!(*operations.state.calls.lock().unwrap(), expected_calls);
1242            let matching_failures = events
1243                .iter()
1244                .filter(|event| {
1245                    matches!(
1246                        event,
1247                        AnalysisProgress::JobFailed { error, .. }
1248                            if error == &expected_error.to_string()
1249                    )
1250                })
1251                .count();
1252            if stage == FailureStage::Cache {
1253                assert_eq!(matching_failures, 0);
1254            } else {
1255                assert_eq!(matching_failures, 1);
1256            }
1257        }
1258    }
1259
1260    #[test]
1261    fn cross_stage_transcript_and_speaker_mismatches_are_rejected() {
1262        let transcript = FakeOperations::successful(1).with_config(|config| {
1263            config.analysis = structured_analysis("different", 1);
1264        });
1265        let (result, events) = run_with_progress(&transcript);
1266        assert_eq!(result, Err(AnalysisError::TranscriptMismatch));
1267        assert!(events.contains(&AnalysisProgress::JobSucceeded { sequence: 6 }));
1268        assert!(!events.contains(&AnalysisProgress::StageCompleted {
1269            stage: AnalysisStage::Structuring,
1270        }));
1271
1272        let speakers = FakeOperations::successful(1).with_config(|config| {
1273            config.analysis = structured_analysis(&config.transcript, 2);
1274        });
1275        let (result, events) = run_with_progress(&speakers);
1276        assert_eq!(result, Err(AnalysisError::SpeakerSetMismatch));
1277        assert!(events.contains(&AnalysisProgress::JobSucceeded { sequence: 6 }));
1278        assert!(!events.contains(&AnalysisProgress::StageCompleted {
1279            stage: AnalysisStage::Structuring,
1280        }));
1281    }
1282
1283    #[test]
1284    fn legacy_api_retains_metadata_and_previous_stage_order() {
1285        for speaker_count in [0, 1, 40] {
1286            let operations = FakeOperations::successful(speaker_count);
1287            let result = block_on(execute_legacy(
1288                &operations,
1289                &legacy_ogg(),
1290                1234,
1291                Some("voice.ogg".into()),
1292            ))
1293            .unwrap();
1294            assert_eq!(result.envelope.audio.duration_ms(), 1234);
1295            assert_eq!(result.envelope.audio.filename(), Some("voice.ogg"));
1296            assert_eq!(
1297                result.envelope.analysis.speakers.len(),
1298                speaker_count as usize
1299            );
1300            assert_eq!(
1301                *operations.state.calls.lock().unwrap(),
1302                ["transcript", "labels", "features", "final"]
1303            );
1304            assert_eq!(
1305                result.label_extractor,
1306                StructurerProvenance {
1307                    model_id: TERRA_MODEL_ID.into(),
1308                    prompt_revision:
1309                        kcode_speaker_v3_llm_protocol::TERRA_SPEAKER_LABELS_PROMPT_REVISION.into(),
1310                }
1311            );
1312        }
1313
1314        let operations = FakeOperations::successful(1);
1315        assert!(matches!(
1316            block_on(execute_legacy(&operations, b"bad", 1, None)),
1317            Err(AnalysisError::Input(_))
1318        ));
1319        assert!(operations.state.calls.lock().unwrap().is_empty());
1320    }
1321
1322    #[test]
1323    fn a_blocked_analysis_does_not_block_an_unrelated_analysis() {
1324        let completed = Arc::new(AtomicBool::new(false));
1325        let fast = FakeOperations::successful(0).with_config(|config| {
1326            config.mark_complete = Some(completed.clone());
1327        });
1328        let slow = FakeOperations::successful(0).with_config(|config| {
1329            config.wait_for = Some(completed.clone());
1330        });
1331        let slow_audio = strict_ogg(48);
1332        let fast_audio = strict_ogg(48);
1333        let (slow_result, fast_result) = block_on(async {
1334            join!(
1335                execute_strict(&slow, &slow_audio, |_| Ok(())),
1336                execute_strict(&fast, &fast_audio, |_| Ok(()))
1337            )
1338        });
1339        slow_result.unwrap();
1340        fast_result.unwrap();
1341        assert!(completed.load(Ordering::SeqCst));
1342    }
1343
1344    #[test]
1345    fn provenance_preserves_the_previous_public_contract() {
1346        let cohort = GeminiCohort::new("gemini-model");
1347        assert_eq!(
1348            cohort.feature_prompt_revisions,
1349            GEMINI_FEATURE_PROMPT_REVISIONS.map(str::to_owned)
1350        );
1351        cohort.validate().unwrap();
1352        StructurerProvenance::new("gpt-5.6").validate().unwrap();
1353        assert_eq!(
1354            GeminiCohort::new(" ").validate(),
1355            Err(ValidationError::Blank("gemini_model_id"))
1356        );
1357    }
1358
1359    #[test]
1360    fn reference_scale_local_orchestration_completes_within_envelope() {
1361        let started = Instant::now();
1362        let operations = FakeOperations::successful(1000);
1363        let (result, _) = run_with_progress(&operations);
1364        assert_eq!(result.unwrap().envelope.analysis.speakers.len(), 1000);
1365        assert!(started.elapsed().as_secs() < 10);
1366    }
1367
1368    #[test]
1369    fn concrete_provider_operations_compile() {
1370        fn require_operations<O: AnalysisOperations>() {}
1371        require_operations::<ProviderOperations>();
1372    }
1373
1374    #[cfg(feature = "providers")]
1375    #[test]
1376    fn legacy_constructor_signature_compiles_without_provider_execution() {
1377        fn require_constructor(
1378            _: fn(kcode_gemini_3_1_pro::Gemini31Pro, kcode_codex_terra::CodexTerra) -> Analyzer,
1379        ) {
1380        }
1381        require_constructor(Analyzer::new);
1382    }
1383
1384    #[cfg(feature = "adapter-providers")]
1385    #[test]
1386    fn adapter_constructor_signature_compiles_without_runtime_execution() {
1387        fn require_constructor(
1388            _: fn(kcode_gemini_3_1_pro::Gemini31Pro, kcode_k1_codex_adapter::Adapter) -> Analyzer,
1389        ) {
1390        }
1391        require_constructor(Analyzer::from_codex_adapter);
1392    }
1393}