Skip to main content

rustev_runtime/
generation.rs

1//! Post-selection generation, bounded field delivery and request caching (037).
2use std::collections::{BTreeMap, BTreeSet, VecDeque};
3use std::future::Future;
4use std::pin::Pin;
5use std::sync::{Arc, Mutex};
6use std::task::{Poll, Waker};
7
8use rustev_contract::definition::StepBody;
9use rustev_contract::descriptor::{BackendDescriptor, OutputKind};
10use rustev_contract::generation::{GenerateRequest, ModelBound, RepairRequest};
11use rustev_contract::generation_result::{FieldOrigin, TextLineage, TextOrigin, TextResult};
12use rustev_contract::judgment::Unresolved;
13use rustev_contract::optimization::{
14    OptimizationAdmission, OptimizationMechanism, OptimizationOutcome,
15};
16use rustev_contract::output::TextValue;
17use rustev_contract::plan::PlanStepDetail;
18use rustev_contract::run::{RequestRecord, RequestResult};
19use rustev_contract::{Document, Identified};
20use rustev_core::evaluate::{SemanticRequest, Supplied};
21use rustev_core::generation_eval::{
22    GenerationError, GenerationEvaluation, GenerationRequest, validate_field,
23};
24use rustev_core::seams::{CancelSignal, GenerationFields};
25
26use crate::driver::{DecisionCtx, RequestOutcome, drive, observation, step_deadline, step_ledger};
27use crate::optimization::{CacheMiss, CacheWrite, KeyParts, RequestKey, request_key};
28use crate::{Inner, Ledger, PreparedPlan};
29
30/// Only checked fields and delivery bookkeeping cross this interface.
31#[derive(Debug, Clone, PartialEq)]
32pub enum GenerationEvent {
33    Field {
34        step: String,
35        item: Vec<String>,
36        field: String,
37        value: TextValue,
38        attempt_id: String,
39        provisional: bool,
40    },
41    Superseded {
42        step: String,
43        item: Vec<String>,
44        field: String,
45        attempt_id: String,
46        origin: FieldOrigin,
47    },
48    Lagged {
49        count: u64,
50    },
51    Complete {
52        text_origin: TextOrigin,
53    },
54}
55
56struct Queue {
57    events: VecDeque<GenerationEvent>,
58    fields: usize,
59    buffer: usize,
60    dropped: u64,
61    closed: bool,
62    waker: Option<Waker>,
63    accepted: Vec<GenerationEvent>,
64}
65
66/// One decision's stream. Dropping the receiver never blocks execution.
67pub struct GenerationStream(Arc<Mutex<Queue>>);
68impl GenerationStream {
69    /// Receive the next event, or `None` once the stream is closed and drained.
70    /// Spec 037 3.13.1 emits `Complete` only after final verification; it defines
71    /// no cancellation or failure event. Cancellation has no text result
72    /// (3.12.6), so it closes without `Complete`, as does a failure before
73    /// verification. `None` alone does not imply success: await the decision
74    /// future for its completion or error. Dropping that future also closes
75    /// the stream. Provisional fields are not final results.
76    pub async fn recv(&mut self) -> Option<GenerationEvent> {
77        std::future::poll_fn(|cx| {
78            let mut q = self.0.lock().unwrap_or_else(|e| e.into_inner());
79            if let Some(event) = q.events.pop_front() {
80                if matches!(event, GenerationEvent::Field { .. }) {
81                    q.fields -= 1;
82                    if q.dropped > 0 {
83                        let count = std::mem::take(&mut q.dropped);
84                        let at = q
85                            .events
86                            .iter()
87                            .position(|e| matches!(e, GenerationEvent::Complete { .. }))
88                            .unwrap_or(q.events.len());
89                        q.events.insert(at, GenerationEvent::Lagged { count });
90                    }
91                }
92                Poll::Ready(Some(event))
93            } else if q.closed {
94                Poll::Ready(None)
95            } else {
96                q.waker = Some(cx.waker().clone());
97                Poll::Pending
98            }
99        })
100        .await
101    }
102}
103
104// Spec 037 3.13.3 reserves control-event slots separately from field slots.
105// Check even contract-bounded inputs before allocating, including on 32-bit hosts.
106fn stream_capacity(
107    mut bounds: impl Iterator<Item = (u64, usize)>,
108    buffer: usize,
109) -> Result<(usize, usize), GenerationError> {
110    let overflow = || GenerationError::InvalidState("generation stream capacity overflow".into());
111    let n = bounds.try_fold(0usize, |total, (requests, fields)| {
112        let requests = usize::try_from(requests).map_err(|_| overflow())?;
113        let fields = requests.checked_mul(fields).ok_or_else(overflow)?;
114        total.checked_add(fields).ok_or_else(overflow)
115    })?;
116    let capacity = n
117        .checked_mul(2)
118        .and_then(|n| n.checked_add(buffer))
119        .and_then(|n| n.checked_add(1))
120        .ok_or_else(overflow)?;
121    Ok((n, capacity))
122}
123
124pub(crate) struct Streaming(Arc<Mutex<Queue>>);
125impl Streaming {
126    pub(crate) fn new(
127        plan: &PreparedPlan,
128        buffer: usize,
129    ) -> Result<(Self, GenerationStream), GenerationError> {
130        if !(1..=4096).contains(&buffer) {
131            return Err(GenerationError::InvalidState(
132                "stream_buffer must be in 1..=4096".into(),
133            ));
134        }
135        let bounds = plan.compiled.plan.steps.iter().filter_map(|s| {
136            let PlanStepDetail::Generate(g) = &s.detail else {
137                return None;
138            };
139            Some((g.worst_case_requests, g.output.fields.len()))
140        });
141        let (n, capacity) = stream_capacity(bounds, buffer)?;
142        let queue = Arc::new(Mutex::new(Queue {
143            events: VecDeque::with_capacity(capacity),
144            fields: 0,
145            buffer,
146            dropped: 0,
147            closed: false,
148            waker: None,
149            accepted: Vec::with_capacity(n),
150        }));
151        Ok((Self(queue.clone()), GenerationStream(queue)))
152    }
153    pub(crate) fn finish(&self, text: &TextResult) {
154        let mut q = self.0.lock().unwrap_or_else(|e| e.into_inner());
155        for event in std::mem::take(&mut q.accepted) {
156            if let GenerationEvent::Field {
157                step,
158                item,
159                field,
160                value,
161                attempt_id,
162                ..
163            } = event
164            {
165                let final_field = text
166                    .steps
167                    .iter()
168                    .find(|s| s.step == step)
169                    .and_then(|s| s.items.iter().find(|i| i.id == item))
170                    .and_then(|i| i.generated.get(&field));
171                // Spec 037 3.10.1 retains a slot for every declared field,
172                // including Absent { reason } with no value. The core's
173                // generation_eval::complete fills these slots; streaming only
174                // accepts declared fields of an existing item. Thus a missing
175                // value still reaches this branch and emits Superseded under
176                // 3.13.2, rather than relying on Complete for reconciliation.
177                if let Some(f) = final_field {
178                    let same = f.value.as_ref() == Some(&value)
179                        && matches!(&f.lineage,
180                        Some(TextLineage::Model { attempt, .. }) if attempt == &attempt_id);
181                    if !same {
182                        q.events.push_back(GenerationEvent::Superseded {
183                            step,
184                            item,
185                            field,
186                            attempt_id,
187                            origin: f.origin.clone(),
188                        });
189                    }
190                }
191            }
192        }
193        q.events.push_back(GenerationEvent::Complete {
194            text_origin: text.text_origin,
195        });
196        let waker = q.waker.take();
197        drop(q);
198        if let Some(w) = waker {
199            w.wake();
200        }
201    }
202}
203impl Drop for Streaming {
204    fn drop(&mut self) {
205        let mut q = self.0.lock().unwrap_or_else(|e| e.into_inner());
206        q.closed = true;
207        let waker = q.waker.take();
208        drop(q);
209        if let Some(w) = waker {
210            w.wake();
211        }
212    }
213}
214
215pub(crate) struct RequestStream {
216    document: GenerateRequest,
217    queue: Arc<Mutex<Queue>>,
218    emitted: Mutex<BTreeSet<String>>,
219}
220pub(crate) struct AttemptFields<'a> {
221    request: &'a RequestStream,
222    attempt: &'a str,
223    pub enabled: bool,
224}
225impl RequestStream {
226    pub(crate) fn attempt<'a>(
227        &'a self,
228        attempt: &'a str,
229        descriptor: &BackendDescriptor,
230    ) -> AttemptFields<'a> {
231        AttemptFields {
232            request: self,
233            attempt,
234            enabled: descriptor.operations.iter().any(|o| {
235                o.operation == rustev_contract::definition::Operation::Generate
236                    && matches!(
237                        o.output,
238                        OutputKind::TextRecord {
239                            streaming: true,
240                            ..
241                        }
242                    )
243            }),
244        }
245    }
246}
247impl GenerationFields for AttemptFields<'_> {
248    fn field(&self, name: &str, value: TextValue) {
249        let r = self.request;
250        let Some(field) = r.document.output.fields.iter().find(|f| f.name == name) else {
251            return;
252        };
253        if validate_field(field, Some(&value), &r.document.vocabularies).is_err() {
254            return;
255        }
256        // Bound all retries of a request to one provisional value per field.
257        if !r
258            .emitted
259            .lock()
260            .unwrap_or_else(|e| e.into_inner())
261            .insert(name.into())
262        {
263            return;
264        }
265        let event = GenerationEvent::Field {
266            step: r.document.step.clone(),
267            item: item_id(&r.document),
268            field: name.into(),
269            value,
270            attempt_id: self.attempt.into(),
271            provisional: true,
272        };
273        let mut q = r.queue.lock().unwrap_or_else(|e| e.into_inner());
274        if q.fields == q.buffer {
275            q.dropped += 1;
276            return;
277        }
278        q.fields += 1;
279        q.accepted.push(event.clone());
280        q.events.push_back(event);
281        let waker = q.waker.take();
282        drop(q);
283        if let Some(w) = waker {
284            w.wake();
285        }
286    }
287}
288fn item_id(doc: &GenerateRequest) -> Vec<String> {
289    let mut id = doc.instance.clone();
290    if matches!(doc.repair, RepairRequest::Round { .. }) {
291        id.pop();
292    }
293    id
294}
295
296pub(crate) struct StageContext<'a> {
297    pub rt: &'a Inner,
298    pub plan: &'a PreparedPlan,
299    pub deadline: u64,
300    pub submitted: u64,
301    pub cancel: &'a CancelSignal,
302    pub ledger: &'a Ledger,
303    pub decision_id: &'a str,
304    pub principal: &'a [u8],
305    pub evaluation_time: rustev_contract::Timestamp,
306    pub streaming: Option<&'a Streaming>,
307}
308pub(crate) struct PendingWrite {
309    key: RequestKey,
310    write: CacheWrite,
311}
312pub(crate) fn write_cache(cx: &StageContext<'_>, writes: Vec<PendingWrite>) {
313    if let Some(memory) = cx
314        .plan
315        .optimization
316        .as_ref()
317        .and_then(|p| p.memory_cache.as_ref())
318    {
319        for mut w in writes {
320            w.write.now = cx.rt.clock.now();
321            // A failed admission cannot invalidate an already delivered result.
322            let _ = cx.rt.optimizer.insert(memory, w.key, w.write);
323        }
324    }
325}
326pub(crate) struct ItemClock {
327    start: Mutex<Option<u64>>,
328    duration: u64,
329}
330impl ItemClock {
331    pub(crate) fn start(&self, now: u64) {
332        self.start
333            .lock()
334            .unwrap_or_else(|e| e.into_inner())
335            .get_or_insert(now);
336    }
337    pub(crate) fn deadline(&self, limit: u64) -> u64 {
338        self.start
339            .lock()
340            .unwrap_or_else(|e| e.into_inner())
341            .map_or(limit, |t| limit.min(t.saturating_add(self.duration)))
342    }
343}
344pub(crate) fn pending_write(out: &RequestOutcome, now: u64) -> Option<PendingWrite> {
345    match (&out.supplied, out.record.attempts.last(), &out.cache) {
346        (Some(Supplied::Output(raw)), Some(a), Some((key, generation)))
347            if a.target == 0 && matches!(a.end, rustev_contract::run::AttemptEnd::Output) =>
348        {
349            Some(PendingWrite {
350                key: key.clone(),
351                write: CacheWrite {
352                    output: raw.clone(),
353                    source_attempt_id: a.attempt_id.clone(),
354                    target: 0,
355                    captured_generation: *generation,
356                    now,
357                },
358            })
359        }
360        _ => None,
361    }
362}
363struct StepBudget {
364    deadline: u64,
365    item_ms: u64,
366    concurrency: usize,
367    ledger: Ledger,
368}
369struct Finished {
370    out: RequestOutcome,
371    source: Option<String>,
372    write: Option<PendingWrite>,
373}
374type Running<'a> = Pin<Box<dyn Future<Output = Result<Finished, GenerationError>> + Send + 'a>>;
375
376/// Items keep their slots through repair; the stage never restarts an item clock.
377pub(crate) async fn run(
378    cx: &StageContext<'_>,
379    stage: &Mutex<GenerationEvaluation<'_>>,
380    mut capture: Option<&mut crate::capture::Capturing<'_>>,
381) -> Result<(Vec<RequestRecord>, Vec<PendingWrite>, bool), GenerationError> {
382    let start = cx.rt.clock.now();
383    let mut budgets = BTreeMap::new();
384    for s in &cx.plan.compiled.plan.definition.steps {
385        if let StepBody::Generate(g) = &s.body {
386            let ModelBound::Literal(item_ms) = g.limits.per_item_deadline_ms else {
387                return Err(GenerationError::InvalidState(
388                    "unresolved generation deadline".into(),
389                ));
390            };
391            budgets.insert(
392                s.id.clone(),
393                StepBudget {
394                    deadline: step_deadline(
395                        cx.deadline,
396                        start,
397                        cx.plan.compiled.plan.definition.limits.deadline_ms,
398                        g.limits.deadline_share,
399                    ),
400                    item_ms,
401                    concurrency: g.limits.concurrency as usize,
402                    ledger: step_ledger(&g.limits.max_cost_units),
403                },
404            );
405        }
406    }
407    let mut active: Vec<(String, Vec<String>, Running<'_>)> = vec![];
408    let mut slots: BTreeMap<(String, Vec<String>), Arc<ItemClock>> = BTreeMap::new();
409    let mut records = BTreeMap::new();
410    let mut seen = BTreeMap::new();
411    let mut writes = vec![];
412    loop {
413        if cx.cancel.is_raised() && active.is_empty() {
414            return Ok((records.into_values().collect(), vec![], true));
415        }
416        let pending = stage.lock().unwrap_or_else(|e| e.into_inner()).pending();
417        for req in &pending {
418            let next = seen.len();
419            seen.entry((req.document.step.clone(), req.document.instance.clone()))
420                .or_insert(next);
421        }
422        // Spec 037 3.12.6 and 003 3.7: drain active work, but admit no new work.
423        for req in pending {
424            if cx.cancel.is_raised() || active.len() >= cx.rt.config.max_parallel_requests {
425                break;
426            }
427            let step = req.owner.clone();
428            let item = item_id(&req.document);
429            if active.iter().any(|(s, i, _)| s == &step && i == &item) {
430                continue;
431            }
432            let Some(budget) = budgets.get(&step) else {
433                return Err(GenerationError::InvalidState(
434                    "unbound generation step".into(),
435                ));
436            };
437            let occupied = slots.keys().filter(|(s, _)| s == &step).count();
438            if !slots.contains_key(&(step.clone(), item.clone())) && occupied >= budget.concurrency
439            {
440                continue;
441            }
442            let item_clock = slots
443                .entry((step.clone(), item.clone()))
444                .or_insert_with(|| {
445                    Arc::new(ItemClock {
446                        start: Mutex::new(None),
447                        duration: budget.item_ms,
448                    })
449                })
450                .clone();
451            active.push((
452                step,
453                item,
454                Box::pin(request(cx, stage, budget, item_clock, req)),
455            ));
456        }
457        if active.is_empty() {
458            break;
459        }
460        let (index, result) = std::future::poll_fn(|task| {
461            for (i, (_, _, f)) in active.iter_mut().enumerate() {
462                if let Poll::Ready(r) = f.as_mut().poll(task) {
463                    return Poll::Ready((i, r));
464                }
465            }
466            Poll::Pending
467        })
468        .await;
469        let (step, item, future) = active.remove(index);
470        drop(future);
471        let Finished { out, source, write } = result?;
472        if let Some(supplied) = out.supplied {
473            let attempt = out
474                .record
475                .attempts
476                .last()
477                .map(|a| a.attempt_id.clone())
478                .or_else(|| {
479                    out.record
480                        .optimization
481                        .as_ref()
482                        .and_then(|o| o.source_attempt_id.clone())
483                })
484                .unwrap_or_else(|| {
485                    format!(
486                        "{}/{}/refused",
487                        crate::driver::escape(cx.decision_id),
488                        crate::driver::escape(&step)
489                    )
490                });
491            let mut g = stage.lock().unwrap_or_else(|e| e.into_inner());
492            if let Some(c) = capture.as_deref_mut() {
493                c.record_generation(&g, cx.plan, &out.record, &supplied);
494            }
495            g.supply_attempt(
496                &out.step,
497                &out.instance,
498                supplied,
499                &attempt,
500                (out.record.attempts.len() as u32).max(1),
501            )?;
502            if let Some(source) = source {
503                g.note_cached_attempt(&attempt, &source);
504            }
505            if !g
506                .pending()
507                .iter()
508                .any(|r| r.owner == step && item_id(&r.document) == item)
509            {
510                slots.remove(&(step, item));
511            }
512        }
513        if let Some(index) = seen.get(&(out.record.step.clone(), out.record.instance.clone())) {
514            records.insert(*index, out.record);
515        }
516        if let Some(write) = write {
517            writes.push(write);
518        }
519    }
520    Ok((
521        records.into_values().collect(),
522        writes,
523        cx.cancel.is_raised(),
524    ))
525}
526
527async fn request(
528    cx: &StageContext<'_>,
529    stage: &Mutex<GenerationEvaluation<'_>>,
530    budget: &StepBudget,
531    item_clock: Arc<ItemClock>,
532    req: GenerationRequest,
533) -> Result<Finished, GenerationError> {
534    let deadline = budget.deadline;
535    let is_screen = req.screen_projection.is_some();
536    let projection = match req.screen_projection {
537        Some(bytes) => bytes,
538        None => req
539            .document
540            .canonical()
541            .map_err(|e| GenerationError::InvalidState(e.to_string()))?,
542    };
543    let semantic = SemanticRequest {
544        step: req.document.step.clone(),
545        instance: req.document.instance.clone(),
546        backend_id: req.backend_id,
547        projection,
548    };
549    let stream = cx.streaming.filter(|_| !is_screen).map(|s| RequestStream {
550        document: req.document,
551        queue: s.0.clone(),
552        emitted: Mutex::new(BTreeSet::new()),
553    });
554    let drive_cx = DecisionCtx {
555        rt: cx.rt,
556        observing: None,
557        plan: cx.plan,
558        deadline,
559        submitted: cx.submitted,
560        cancel: cx.cancel,
561        ledger: cx.ledger,
562        decision_id: cx.decision_id,
563        principal: cx.principal,
564        evaluation_time: cx.evaluation_time,
565        ev: None,
566        generation: Some(stage),
567        step_ledger: Some(&budget.ledger),
568        stream: stream.as_ref(),
569        item_clock: Some(&item_clock),
570        // drive() does not admit to cache; only run_requests() uses this
571        // queue. Generation returns its pending write to the delivery gate.
572        deferred_cache: None,
573        live: None,
574    };
575    let mut cache = None;
576    let mut diagnostics = vec![];
577    if !is_screen
578        && !cx.cancel.is_raised()
579        && cx.rt.clock.now() < item_clock.deadline(deadline)
580        && let Some(policy) = &cx.plan.optimization
581        && policy.memory_cache.is_some()
582        && let Some(target) = cx
583            .plan
584            .steps
585            .get(&semantic.step)
586            .and_then(|s| s.targets.first())
587        && let Ok(descriptor) = target.registered.backend.descriptor().id()
588    {
589        let key = request_key(KeyParts {
590            plan_id: &cx.plan.compiled.id,
591            policy,
592            principal: cx.principal,
593            backend_artifact: &target.artifact,
594            backend_descriptor: &descriptor,
595            step: &semantic.step,
596            instance: &semantic.instance,
597            projection: &semantic.projection,
598        });
599        let generation = cx.rt.optimizer.generation(&key.namespace);
600        let lookup = cx.rt.optimizer.lookup(&key, cx.rt.clock.now());
601        if let Ok(hit) = lookup {
602            item_clock.start(cx.rt.clock.now());
603            let mut o = observation(
604                policy,
605                OptimizationMechanism::MemoryCache,
606                OptimizationOutcome::Hit,
607                hit.generation,
608                vec![],
609            );
610            o.entry_id = Some(hit.entry_id);
611            o.age_ms = hit.age_ms;
612            o.source_attempt_id = Some(hit.source_attempt_id.clone());
613            let source = hit
614                .source_attempt_id
615                .split('/')
616                .next()
617                .map(crate::driver::unescape)
618                .unwrap_or_default();
619            let record = RequestRecord {
620                screen: None,
621                step: semantic.step.clone(),
622                instance: semantic.instance.clone(),
623                result: RequestResult::Output { target: hit.target },
624                attempts: vec![],
625                transitions: vec![],
626                optimization: Some(o),
627            };
628            return Ok(Finished {
629                out: RequestOutcome {
630                    step: semantic.step,
631                    instance: semantic.instance,
632                    supplied: Some(Supplied::Output(hit.output)),
633                    record,
634                    cache: None,
635                },
636                source: Some(source),
637                write: None,
638            });
639        }
640        if let Err(miss) = lookup {
641            let detail = crate::driver::cache_miss_detail(miss);
642            if miss == CacheMiss::Collision && policy.admission == OptimizationAdmission::Refuse {
643                item_clock.start(cx.rt.clock.now());
644                let reason = Unresolved::BackendUnavailable {
645                    detail: detail.into(),
646                };
647                let record = RequestRecord {
648                    screen: None,
649                    step: semantic.step.clone(),
650                    instance: semantic.instance.clone(),
651                    result: RequestResult::Failed(reason.clone()),
652                    attempts: vec![],
653                    transitions: vec![],
654                    optimization: Some(observation(
655                        policy,
656                        OptimizationMechanism::MemoryCache,
657                        OptimizationOutcome::Refused,
658                        generation,
659                        vec![detail.into()],
660                    )),
661                };
662                return Ok(Finished {
663                    out: RequestOutcome {
664                        step: semantic.step,
665                        instance: semantic.instance,
666                        supplied: Some(Supplied::Failed(reason)),
667                        record,
668                        cache: None,
669                    },
670                    source: None,
671                    write: None,
672                });
673            }
674            diagnostics.push(detail.into());
675        }
676        cache = Some((key, generation));
677    }
678    let mut out = drive(
679        &drive_cx,
680        semantic,
681        cache.as_ref().map(|(k, _)| k.clone()),
682        cache.as_ref().map_or(0, |(_, g)| *g),
683        diagnostics,
684    )
685    .await;
686    // Unlike driver::run_requests(), this path calls drive() directly and
687    // never calls admit_to_cache(), which normally invokes record_cache_cost.
688    // Account exactly once here (the cache-hit early return has no new cost),
689    // then stage the write for the delivery gate under spec 037 3.12.7/3.14.1.
690    crate::driver::record_cache_cost(&mut out);
691    let write = if is_screen {
692        None
693    } else {
694        pending_write(&out, cx.rt.clock.now())
695    };
696    Ok(Finished {
697        out,
698        source: None,
699        write,
700    })
701}
702
703#[cfg(test)]
704mod tests {
705    use super::*;
706
707    #[test]
708    fn stream_capacity_rejects_overflow_before_allocation() {
709        struct Case {
710            label: &'static str,
711            bounds: Vec<(u64, usize)>,
712            buffer: usize,
713        }
714
715        let max = usize::MAX as u64;
716        for Case {
717            label,
718            bounds,
719            buffer,
720        } in [
721            Case {
722                label: "Per-step multiplication",
723                bounds: vec![(max, 2)],
724                buffer: 1,
725            },
726            Case {
727                label: "Sum across steps",
728                bounds: vec![(max, 1), (1, 1)],
729                buffer: 1,
730            },
731            Case {
732                label: "Control-event multiplication",
733                bounds: vec![(max, 1)],
734                buffer: 1,
735            },
736            Case {
737                label: "Field-buffer addition",
738                bounds: vec![(max / 2, 1)],
739                // (usize::MAX / 2) * 2 = usize::MAX - 1; + 2 overflows at checked_add(buffer).
740                buffer: 2,
741            },
742            Case {
743                label: "Terminal-event addition",
744                bounds: vec![(max / 2, 1)],
745                // (usize::MAX / 2) * 2 + 1 = usize::MAX; terminal checked_add(1) overflows.
746                buffer: 1,
747            },
748            Case {
749                label: "Conversion on narrower targets",
750                bounds: vec![(u64::MAX, 2)],
751                buffer: 1,
752            },
753        ] {
754            assert!(
755                matches!(
756                    stream_capacity(bounds.into_iter(), buffer),
757                    Err(GenerationError::InvalidState(detail))
758                        if detail == "generation stream capacity overflow"
759                ),
760                "{label} (buffer = {buffer}) must reject capacity overflow"
761            );
762        }
763    }
764
765    #[test]
766    fn stream_capacity_reserves_control_events_separately() {
767        assert_eq!(
768            stream_capacity([(2, 3), (4, 2)].into_iter(), 1).unwrap(),
769            (14, 30)
770        );
771        assert_eq!(
772            stream_capacity(std::iter::empty(), 4096).unwrap(),
773            (0, 4097)
774        );
775    }
776}