Skip to main content

rustev_runtime/
optimization.rs

1//! Bounded reusable-work state for spec 017.
2//!
3//! State is kept per cache namespace, which is per plan: one plan's bounds
4//! never evict another plan's entries, and a per-scope bound evicts only the
5//! inserting scope's own oldest entries (I-2).
6
7use std::collections::{BTreeMap, VecDeque};
8use std::sync::{Arc, Mutex};
9
10use rustev_contract::canonical::{record_canonical_bytes, tagged_digest};
11use rustev_contract::ids::{ArtifactId, DescriptorId, PlanId};
12use rustev_contract::optimization::{MemoryCachePolicy, OptimizationPolicy};
13use rustev_contract::output::RawOutput;
14
15use crate::clock::Instant;
16
17const KEY_TAG: &str = "rustev.optimization-key/1";
18const ENTRY_TAG: &str = "rustev.optimization-entry/1";
19
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub(crate) struct RequestKey {
22    pub id: String,
23    pub material: Vec<u8>,
24    pub scope_id: String,
25    pub namespace: String,
26}
27
28impl RequestKey {
29    /// The request key over `material`, as a decision entry of the
30    /// persistent tier retains it (spec 044, 3.6.4).
31    pub(crate) fn from_material(material: Vec<u8>, scope_id: String, namespace: String) -> Self {
32        RequestKey {
33            id: tagged_digest(KEY_TAG, &material),
34            material,
35            scope_id,
36            namespace,
37        }
38    }
39}
40
41/// Every output-affecting member available at the runtime seam.
42///
43/// The plan id transitively identifies operation, task, question, ordered
44/// options or levels, ordered candidates, preprocessing, tokenization,
45/// truncation, precision, backend configuration, requested privacy,
46/// served-identity requirements, step semantics, calibration, fallback,
47/// protocol, adapter, artifact binding and the optimization policy. The
48/// canonical projection identifies the concrete projected values, instance,
49/// canonical inputs and provenance-relevant revisions. Descriptor and
50/// artifact ids bind the selected installed target. `principal` is the
51/// host-supplied complete authorized-scope handle. `profile` is the plan's
52/// profile identity or `none` (spec 044, 3.5.2), held apart from the plan
53/// id so no profile can collide with none. Runtime and key-schema versions
54/// prevent reuse across implementation or key interpretation.
55pub(crate) struct KeyParts<'a> {
56    pub plan_id: &'a PlanId,
57    pub profile: &'a str,
58    pub policy: &'a OptimizationPolicy,
59    pub principal: &'a [u8],
60    pub backend_artifact: &'a ArtifactId,
61    pub backend_descriptor: &'a DescriptorId,
62    pub step: &'a str,
63    pub instance: &'a [String],
64    pub projection: &'a [u8],
65}
66
67fn part(out: &mut Vec<u8>, bytes: &[u8]) {
68    out.extend_from_slice(&(bytes.len() as u64).to_be_bytes());
69    out.extend_from_slice(bytes);
70}
71
72pub(crate) fn request_key(p: KeyParts<'_>) -> RequestKey {
73    let mut material = Vec::new();
74    part(&mut material, p.plan_id.as_str().as_bytes());
75    part(&mut material, p.profile.as_bytes());
76    part(&mut material, &p.policy.key_schema_version.to_be_bytes());
77    part(
78        &mut material,
79        &p.policy.cache_namespace_version.to_be_bytes(),
80    );
81    part(&mut material, env!("CARGO_PKG_VERSION").as_bytes());
82    part(&mut material, p.principal);
83    part(&mut material, p.backend_artifact.as_str().as_bytes());
84    part(&mut material, p.backend_descriptor.as_str().as_bytes());
85    part(&mut material, p.step.as_bytes());
86    part(&mut material, &(p.instance.len() as u64).to_be_bytes());
87    for member in p.instance {
88        part(&mut material, member.as_bytes());
89    }
90    part(&mut material, p.projection);
91    let scope_id = scope_id(p.principal);
92    let namespace = namespace(p.plan_id, p.policy);
93    RequestKey {
94        id: tagged_digest(KEY_TAG, &material),
95        material,
96        scope_id,
97        namespace,
98    }
99}
100
101/// The plan's profile identity for request keys (spec 044, 3.5.2): the
102/// JSON array of the profile ids its instantiation applied, in chain
103/// order, or `none`. An array delimits every id, and never reads `none`.
104/// The match is exhaustive, so a new chain variant must be given its own
105/// encoding here and can never share another's key.
106pub(crate) fn profile_of(plan: &rustev_contract::plan::Plan) -> String {
107    use rustev_contract::plan::ProfileChain;
108    match plan.instantiation.as_ref().map(|i| &i.profile) {
109        None | Some(ProfileChain::None) => "none".into(),
110        // An empty chain is `[]`: still an array, never `none`.
111        Some(ProfileChain::Chain(entries)) => {
112            profile_ids(&entries.iter().map(|e| e.id.as_str()).collect::<Vec<_>>())
113        }
114    }
115}
116
117fn profile_ids(ids: &[&str]) -> String {
118    serde_json::to_string(ids).expect("a list of strings serializes")
119}
120
121pub(crate) fn scope_id(principal: &[u8]) -> String {
122    tagged_digest("rustev.optimization-scope/1", principal)
123}
124
125pub(crate) fn namespace(plan_id: &PlanId, policy: &OptimizationPolicy) -> String {
126    format!(
127        "{}/{}/{}",
128        plan_id, policy.key_schema_version, policy.cache_namespace_version
129    )
130}
131
132#[derive(Debug, Clone, PartialEq)]
133pub(crate) struct CachedOutput {
134    pub output: RawOutput,
135    pub source_attempt_id: String,
136    pub target: u32,
137    pub entry_id: String,
138    pub age_ms: u64,
139    pub generation: u64,
140}
141
142pub(crate) struct CacheWrite {
143    pub output: RawOutput,
144    pub source_attempt_id: String,
145    pub target: u32,
146    pub captured_generation: u64,
147    pub now: Instant,
148}
149
150#[derive(Debug, Clone, PartialEq, Eq)]
151pub(crate) struct CacheAdmission {
152    pub entry_id: String,
153    pub evicted_entries: u64,
154    pub evicted_bytes: u64,
155}
156
157/// Why a lookup did not hit.
158#[derive(Debug, Clone, Copy, PartialEq, Eq)]
159pub(crate) enum CacheMiss {
160    Absent,
161    Expired,
162    /// Equal digests over different key material: an integrity event.
163    Collision,
164}
165
166/// Why a validated output was not admitted. Nothing is touched before the
167/// admissibility preconditions hold.
168#[derive(Debug, Clone, Copy, PartialEq, Eq)]
169pub(crate) enum CacheRefusal {
170    /// The output has no canonical form.
171    Uncanonical,
172    /// The entry exceeds the per-entry bound.
173    EntryTooLarge,
174    /// The namespace generation advanced after the request captured it.
175    StaleGeneration,
176    /// A retained entry has the same digest and different key material.
177    Collision,
178    /// The policy's memory tier holds no entry: it has no `max_entries`
179    /// (no memory tier, spec 044 3.1) or a bound of 0, which the compiler
180    /// refuses. Nothing is admitted, never an unbounded or default tier.
181    NoCapacity,
182}
183
184#[derive(Debug, Clone)]
185enum CachedValue {
186    Semantic(RawOutput),
187    Page(Vec<u8>),
188}
189
190struct StoredWrite {
191    output: CachedValue,
192    source_attempt_id: String,
193    target: u32,
194    captured_generation: u64,
195    now: Instant,
196}
197
198pub(crate) struct PageWrite {
199    pub bytes: Vec<u8>,
200    pub attempt: String,
201    pub generation: u64,
202    pub now: Instant,
203}
204
205#[derive(Debug, Clone)]
206struct Entry {
207    key: RequestKey,
208    output: CachedValue,
209    source_attempt_id: String,
210    target: u32,
211    entry_id: String,
212    created: Instant,
213    expires: Instant,
214    generation: u64,
215    bytes: u64,
216    /// Admission sequence, which makes lazily deleted queue slots
217    /// recognizable.
218    seq: u64,
219}
220
221type Slot = (String, u64);
222
223#[derive(Debug, Default)]
224struct NamespaceState {
225    generation: u64,
226    entries: BTreeMap<String, Entry>,
227    fifo: VecDeque<Slot>,
228    scope_fifo: BTreeMap<String, VecDeque<Slot>>,
229    scope_bytes: BTreeMap<String, u64>,
230    total_bytes: u64,
231    next_seq: u64,
232    /// The decision-result cache's memory tier (spec 044, 3.6), under the
233    /// same generation and with its own entry bound.
234    decisions: BTreeMap<String, decision::DecisionSlot>,
235    decision_fifo: VecDeque<Slot>,
236}
237
238impl NamespaceState {
239    fn live(&self, slot: &Slot) -> bool {
240        self.entries.get(&slot.0).is_some_and(|e| e.seq == slot.1)
241    }
242
243    fn remove(&mut self, id: &str) -> Option<Entry> {
244        let entry = self.entries.remove(id)?;
245        self.total_bytes = self.total_bytes.saturating_sub(entry.bytes);
246        if let Some(bytes) = self.scope_bytes.get_mut(&entry.key.scope_id) {
247            *bytes = bytes.saturating_sub(entry.bytes);
248            if *bytes == 0 {
249                self.scope_bytes.remove(&entry.key.scope_id);
250            }
251        }
252        // Queue slots are deleted lazily; compact when stale slots dominate
253        // so the queues stay proportional to the live entries.
254        if self.fifo.len() > 2 * self.entries.len() + 16 {
255            let entries = &self.entries;
256            self.fifo
257                .retain(|s| entries.get(&s.0).is_some_and(|e| e.seq == s.1));
258        }
259        if let Some(queue) = self.scope_fifo.get_mut(&entry.key.scope_id) {
260            let live = self.scope_bytes.contains_key(&entry.key.scope_id);
261            if !live {
262                self.scope_fifo.remove(&entry.key.scope_id);
263            } else if queue.len() > 16 {
264                let entries = &self.entries;
265                queue.retain(|s| entries.get(&s.0).is_some_and(|e| e.seq == s.1));
266            }
267        }
268        Some(entry)
269    }
270
271    /// Evict the oldest live entry of `scope`, or of the namespace.
272    fn evict_oldest(&mut self, scope: Option<&str>) -> Option<Entry> {
273        loop {
274            let slot = match scope {
275                Some(scope) => self.scope_fifo.get_mut(scope)?.pop_front()?,
276                None => self.fifo.pop_front()?,
277            };
278            if self.live(&slot) {
279                return self.remove(&slot.0);
280            }
281        }
282    }
283}
284
285/// Which retained entries an invalidation selects.
286#[derive(Debug, Clone, PartialEq, Eq)]
287pub(crate) enum InvalidationSelector {
288    Namespace { namespace: String },
289    Scope { namespace: String, scope_id: String },
290}
291
292#[derive(Debug, Clone, PartialEq, Eq)]
293pub struct InvalidationResult {
294    /// The memory tier's namespace generation after the invalidation.
295    /// Results captured under an earlier generation are never stored.
296    pub generation: u64,
297    pub removed_entries: u64,
298    pub removed_bytes: u64,
299    /// The persistent tier's result, when the plan has one (spec 044,
300    /// 3.10.3).
301    pub persistent: Option<PersistentInvalidation>,
302    /// Whether every tier of the selection is invalidated. Until it is, the
303    /// selection's persistent entries are neither read nor written.
304    pub complete: bool,
305}
306
307/// The persistent tier's part of an invalidation (spec 044, 3.10.3).
308#[derive(Debug, Clone, PartialEq, Eq)]
309pub enum PersistentInvalidation {
310    /// Marked unavailable; `Runtime::invalidate` has not completed it.
311    Pending,
312    /// The store advanced the selected generation.
313    Done { generation: u64, removed: u64 },
314    /// The store failed; the selection stays unavailable until a later
315    /// `Runtime::invalidate` reconciles it (spec 017, 3.7).
316    Failed { detail: String },
317}
318
319#[derive(Debug, Default)]
320pub(crate) struct Optimizer {
321    namespaces: Mutex<BTreeMap<String, NamespaceState>>,
322    /// Shared calls in flight, per plan namespace (spec 044, 3.2): a key
323    /// holds the plan, so no attempt is shared across plans, and each plan's
324    /// `max_shared_calls` bounds its own slots.
325    shared: Mutex<BTreeMap<String, crate::driver::SharedCalls>>,
326}
327
328impl Optimizer {
329    pub(crate) fn shared_calls(
330        &self,
331        namespace: &str,
332        max_slots: usize,
333    ) -> crate::driver::SharedCalls {
334        self.shared
335            .lock()
336            .unwrap_or_else(|e| e.into_inner())
337            .entry(namespace.to_string())
338            .or_insert_with(|| Arc::new(crate::shared::Registry::new(max_slots)))
339            .clone()
340    }
341
342    fn lock(&self) -> std::sync::MutexGuard<'_, BTreeMap<String, NamespaceState>> {
343        self.namespaces.lock().unwrap_or_else(|e| e.into_inner())
344    }
345
346    pub(crate) fn generation(&self, namespace: &str) -> u64 {
347        self.lock().get(namespace).map_or(0, |ns| ns.generation)
348    }
349
350    fn lookup_entry(&self, key: &RequestKey, now: Instant) -> Result<Entry, CacheMiss> {
351        let mut namespaces = self.lock();
352        let Some(ns) = namespaces.get_mut(&key.namespace) else {
353            return Err(CacheMiss::Absent);
354        };
355        let Some(entry) = ns.entries.get(&key.id) else {
356            return Err(CacheMiss::Absent);
357        };
358        if entry.key.material != key.material {
359            return Err(CacheMiss::Collision);
360        }
361        if entry.expires <= now || entry.generation != ns.generation {
362            ns.remove(&key.id);
363            return Err(CacheMiss::Expired);
364        }
365        Ok(entry.clone())
366    }
367
368    pub(crate) fn lookup(&self, key: &RequestKey, now: Instant) -> Result<CachedOutput, CacheMiss> {
369        let e = self.lookup_entry(key, now)?;
370        let CachedValue::Semantic(output) = e.output else {
371            return Err(CacheMiss::Collision);
372        };
373        Ok(CachedOutput {
374            output,
375            source_attempt_id: e.source_attempt_id,
376            target: e.target,
377            entry_id: e.entry_id,
378            age_ms: now.saturating_sub(e.created),
379            generation: e.generation,
380        })
381    }
382
383    pub(crate) fn lookup_page(
384        &self,
385        key: &RequestKey,
386        now: Instant,
387    ) -> Result<(Vec<u8>, u64), CacheMiss> {
388        let e = self.lookup_entry(key, now)?;
389        let CachedValue::Page(bytes) = e.output else {
390            return Err(CacheMiss::Collision);
391        };
392        Ok((bytes, now.saturating_sub(e.created)))
393    }
394
395    pub(crate) fn insert_page(
396        &self,
397        policy: &MemoryCachePolicy,
398        key: RequestKey,
399        page: PageWrite,
400    ) -> Result<CacheAdmission, CacheRefusal> {
401        self.insert_value(
402            policy,
403            key,
404            StoredWrite {
405                source_attempt_id: page.attempt,
406                target: 0,
407                captured_generation: page.generation,
408                now: page.now,
409                output: CachedValue::Page(page.bytes),
410            },
411        )
412    }
413
414    pub(crate) fn insert(
415        &self,
416        policy: &MemoryCachePolicy,
417        key: RequestKey,
418        write: CacheWrite,
419    ) -> Result<CacheAdmission, CacheRefusal> {
420        self.insert_value(
421            policy,
422            key,
423            StoredWrite {
424                output: CachedValue::Semantic(write.output),
425                source_attempt_id: write.source_attempt_id,
426                target: write.target,
427                captured_generation: write.captured_generation,
428                now: write.now,
429            },
430        )
431    }
432
433    fn insert_value(
434        &self,
435        policy: &MemoryCachePolicy,
436        key: RequestKey,
437        write: StoredWrite,
438    ) -> Result<CacheAdmission, CacheRefusal> {
439        let output_bytes = match &write.output {
440            CachedValue::Semantic(output) => {
441                record_canonical_bytes(output).map_err(|_| CacheRefusal::Uncanonical)?
442            }
443            CachedValue::Page(bytes) => bytes.clone(),
444        };
445        let bytes = key
446            .material
447            .len()
448            .saturating_add(output_bytes.len())
449            .saturating_add(write.source_attempt_id.len()) as u64;
450        // Core validation guarantees max_entry_bytes <= max_scope_bytes <=
451        // max_bytes and max_entries >= 1, so an entry within its own bound
452        // is always admissible after eviction.
453        if bytes > policy.max_entry_bytes {
454            return Err(CacheRefusal::EntryTooLarge);
455        }
456        let mut namespaces = self.lock();
457        let ns = namespaces.entry(key.namespace.clone()).or_default();
458        if ns.generation != write.captured_generation {
459            return Err(CacheRefusal::StaleGeneration);
460        }
461        if ns
462            .entries
463            .get(&key.id)
464            .is_some_and(|entry| entry.key.material != key.material)
465        {
466            return Err(CacheRefusal::Collision);
467        }
468        // Replacing an equivalent entry is not an eviction.
469        ns.remove(&key.id);
470        let mut evicted_entries = 0_u64;
471        let mut evicted_bytes = 0_u64;
472        let mut evicted = |entry: Entry| {
473            evicted_entries += 1;
474            evicted_bytes = evicted_bytes.saturating_add(entry.bytes);
475        };
476        while ns
477            .scope_bytes
478            .get(&key.scope_id)
479            .copied()
480            .unwrap_or(0)
481            .saturating_add(bytes)
482            > policy.max_scope_bytes
483        {
484            let Some(entry) = ns.evict_oldest(Some(&key.scope_id)) else {
485                break;
486            };
487            evicted(entry);
488        }
489        while ns.entries.len() as u64 >= policy.max_entries
490            || ns.total_bytes.saturating_add(bytes) > policy.max_bytes
491        {
492            let Some(entry) = ns.evict_oldest(None) else {
493                break;
494            };
495            evicted(entry);
496        }
497        let mut entry_material = key.material.clone();
498        part(&mut entry_material, &output_bytes);
499        part(&mut entry_material, write.source_attempt_id.as_bytes());
500        let entry_id = tagged_digest(ENTRY_TAG, &entry_material);
501        let seq = ns.next_seq;
502        ns.next_seq += 1;
503        *ns.scope_bytes.entry(key.scope_id.clone()).or_default() += bytes;
504        ns.total_bytes += bytes;
505        ns.fifo.push_back((key.id.clone(), seq));
506        ns.scope_fifo
507            .entry(key.scope_id.clone())
508            .or_default()
509            .push_back((key.id.clone(), seq));
510        let generation = ns.generation;
511        ns.entries.insert(
512            key.id.clone(),
513            Entry {
514                key,
515                output: write.output,
516                source_attempt_id: write.source_attempt_id,
517                target: write.target,
518                entry_id: entry_id.clone(),
519                created: write.now,
520                expires: write.now.saturating_add(policy.ttl_ms),
521                generation,
522                bytes,
523                seq,
524            },
525        );
526        Ok(CacheAdmission {
527            entry_id,
528            evicted_entries,
529            evicted_bytes,
530        })
531    }
532
533    /// Advance the namespace generation, then remove the selected entries.
534    /// Any result captured under the old generation is refused at insert.
535    pub(crate) fn invalidate(&self, selector: &InvalidationSelector) -> InvalidationResult {
536        let namespace = match selector {
537            InvalidationSelector::Namespace { namespace }
538            | InvalidationSelector::Scope { namespace, .. } => namespace,
539        };
540        let mut namespaces = self.lock();
541        let ns = namespaces.entry(namespace.clone()).or_default();
542        ns.generation = ns.generation.saturating_add(1);
543        let ids: Vec<String> = ns
544            .entries
545            .iter()
546            .filter(|(_, entry)| match selector {
547                InvalidationSelector::Namespace { .. } => true,
548                InvalidationSelector::Scope { scope_id, .. } => &entry.key.scope_id == scope_id,
549            })
550            .map(|(id, _)| id.clone())
551            .collect();
552        let mut removed_bytes = 0;
553        for id in &ids {
554            if let Some(entry) = ns.remove(id) {
555                removed_bytes += entry.bytes;
556            }
557        }
558        let decisions: Vec<String> = ns
559            .decisions
560            .iter()
561            .filter(|(_, slot)| match selector {
562                InvalidationSelector::Namespace { .. } => true,
563                InvalidationSelector::Scope { scope_id, .. } => &slot.key.scope_id == scope_id,
564            })
565            .map(|(id, _)| id.clone())
566            .collect();
567        for id in &decisions {
568            if let Some(slot) = ns.decisions.remove(id) {
569                removed_bytes += slot.bytes;
570            }
571        }
572        InvalidationResult {
573            generation: ns.generation,
574            removed_entries: (ids.len() + decisions.len()) as u64,
575            removed_bytes,
576            persistent: None,
577            complete: true,
578        }
579    }
580
581    #[cfg(test)]
582    fn force_collision(&self, key: &RequestKey, other_material: Vec<u8>) {
583        let mut namespaces = self.lock();
584        if let Some(entry) = namespaces
585            .get_mut(&key.namespace)
586            .and_then(|ns| ns.entries.get_mut(&key.id))
587        {
588            entry.key.material = other_material;
589        }
590    }
591
592    #[cfg(test)]
593    fn total_bytes(&self, namespace: &str) -> u64 {
594        self.lock().get(namespace).map_or(0, |ns| ns.total_bytes)
595    }
596
597    #[cfg(test)]
598    fn queue_lengths(&self, namespace: &str) -> (usize, usize) {
599        let namespaces = self.lock();
600        let ns = &namespaces[namespace];
601        (
602            ns.fifo.len(),
603            ns.scope_fifo.values().map(VecDeque::len).sum(),
604        )
605    }
606}
607
608/// Acquisition identity and complete authorized scope, in the versioned cache namespace.
609pub(crate) fn acquisition_key(
610    plan: &PlanId,
611    policy: &OptimizationPolicy,
612    principal: &[u8],
613    request: &rustev_core::evaluate::PendingAcquisition,
614) -> RequestKey {
615    let mut material = Vec::new();
616    part(&mut material, b"acquisition-page");
617    part(&mut material, &policy.key_schema_version.to_be_bytes());
618    // Like `request_key`, bind the runtime version to prevent reuse across
619    // implementations (spec 017, Implementation record: complete memory-cache
620    // key). Spec 036 3.13.7 uses that versioned key scheme.
621    part(&mut material, env!("CARGO_PKG_VERSION").as_bytes());
622    // `AcquisitionRequestId` is the identity of the whole
623    // `rustev.acquisition-request/1` document, which names its `operation`
624    // (spec 036 3.3.1), so two operations never share a key.
625    part(&mut material, request.request_id.as_str().as_bytes());
626    part(&mut material, request.request.source.as_str().as_bytes());
627    part(&mut material, principal);
628    RequestKey {
629        id: tagged_digest(KEY_TAG, &material),
630        material,
631        scope_id: scope_id(principal),
632        namespace: namespace(plan, policy),
633    }
634}
635
636// ---------------------------------------------------------------------------
637// The decision-result cache (spec 044, 3.5 and 3.6)
638// ---------------------------------------------------------------------------
639
640pub(crate) mod decision {
641    //! The decision-result cache's key, memory tier and per-decision run
642    //! (spec 044, 3.5 and 3.6).
643
644    use std::collections::{BTreeMap, BTreeSet};
645    use std::sync::{Arc, Mutex, MutexGuard};
646
647    use rustev_contract::cache_entry::EntryGenerations;
648    use rustev_contract::canonical::{record_canonical_bytes, tagged_digest};
649    use rustev_contract::ids::{ContentDigest, PlanId};
650    use rustev_contract::optimization::{
651        DecisionCachePolicy, DecisionCacheTier, OptimizationPolicy,
652    };
653    use rustev_contract::output::RawOutput;
654    use rustev_contract::scope::Handle;
655    use rustev_contract::snapshot::Snapshot;
656    use rustev_contract::time::Timestamp;
657
658    use super::{CacheRefusal, Optimizer, RequestKey, namespace, part, profile_of, scope_id};
659
660    const DECISION_KEY_TAG: &str = "rustev.decision-key/1";
661    const DECISION_ENTRY_TAG: &str = "rustev.decision-entry/1";
662    const DECISION_INPUT_TAG: &str = "rustev.decision-input/1";
663
664    /// What a decision key is derived from: the plan's compiled
665    /// `decision_key_schema`, and the decision's scope, dimensions and
666    /// snapshot (spec 044, 3.5 and 3.6.1).
667    pub(crate) struct DecisionKeyParts<'a> {
668        pub schema: &'a [String],
669        pub plan_id: &'a PlanId,
670        pub profile: &'a str,
671        pub policy: &'a OptimizationPolicy,
672        pub principal: &'a [u8],
673        pub dimensions: &'a BTreeMap<String, Handle>,
674        pub snapshot: &'a Snapshot,
675    }
676
677    /// Why a decision has no key, so is neither served from nor admitted to
678    /// the decision cache (spec 044, 3.5 items 3 and 4).
679    #[derive(Debug, Clone, PartialEq, Eq)]
680    pub(crate) enum Unkeyed {
681        /// A declared dimension the host did not supply.
682        DimensionMissing(String),
683        /// A dimension the plan does not declare, a member this runtime cannot
684        /// derive, or a snapshot entry without canonical form.
685        Ineligible(String),
686    }
687
688    impl Unkeyed {
689        pub(crate) fn diagnostic(&self) -> String {
690            match self {
691                Unkeyed::DimensionMissing(d) => format!("key_dimension_missing: {d:?}"),
692                Unkeyed::Ineligible(why) => format!("key_ineligible: {why}"),
693            }
694        }
695    }
696
697    /// The decision-level key (spec 044, 3.6.1): every member of the compiled
698    /// schema in order, each named in the material beside its value. An input
699    /// member holds the digest of each of the input's snapshot entries as
700    /// supplied, in snapshot order; a volatile input is not a member.
701    pub(crate) fn decision_key(p: DecisionKeyParts<'_>) -> Result<RequestKey, Unkeyed> {
702        if let Some(extra) = p
703            .dimensions
704            .keys()
705            .find(|d| !p.schema.contains(&format!("dimension:{d}")))
706        {
707            return Err(Unkeyed::Ineligible(format!(
708                "dimension {extra:?} is not declared by the plan"
709            )));
710        }
711        let mut material = Vec::new();
712        for member in p.schema {
713            part(&mut material, member.as_bytes());
714            match member.as_str() {
715                "plan_id" => part(&mut material, p.plan_id.as_str().as_bytes()),
716                "key_schema_version" => {
717                    part(&mut material, &p.policy.key_schema_version.to_be_bytes())
718                }
719                "cache_namespace_version" => part(
720                    &mut material,
721                    &p.policy.cache_namespace_version.to_be_bytes(),
722                ),
723                "runtime_version" => part(&mut material, env!("CARGO_PKG_VERSION").as_bytes()),
724                "scope" => part(&mut material, p.principal),
725                "profile" => part(&mut material, p.profile.as_bytes()),
726                m => {
727                    if let Some(name) = m.strip_prefix("dimension:") {
728                        let handle = p
729                            .dimensions
730                            .get(name)
731                            .ok_or_else(|| Unkeyed::DimensionMissing(name.into()))?;
732                        part(&mut material, handle.as_str().as_bytes());
733                    } else if let Some(name) = m.strip_prefix("input:") {
734                        let entries: Vec<_> = p
735                            .snapshot
736                            .entries
737                            .iter()
738                            .filter(|e| e.field == name)
739                            .collect();
740                        part(&mut material, &(entries.len() as u64).to_be_bytes());
741                        for e in entries {
742                            let bytes = record_canonical_bytes(e).map_err(|_| {
743                                Unkeyed::Ineligible(format!("input {name:?} has no canonical form"))
744                            })?;
745                            part(
746                                &mut material,
747                                tagged_digest(DECISION_INPUT_TAG, &bytes).as_bytes(),
748                            );
749                        }
750                    } else {
751                        return Err(Unkeyed::Ineligible(format!("unknown key member {m:?}")));
752                    }
753                }
754            }
755        }
756        Ok(RequestKey {
757            id: tagged_digest(DECISION_KEY_TAG, &material),
758            material,
759            scope_id: scope_id(p.principal),
760            namespace: namespace(p.plan_id, p.policy),
761        })
762    }
763
764    /// A request outside the refresh closure as a decision entry holds it
765    /// (spec 044, 3.6.4): its 017 key, the validated output of target 0 and the
766    /// attempt that produced it. Its derivation follows from the plan the key
767    /// names, and its lineage is that attempt and the entry's source record.
768    #[derive(Debug, Clone)]
769    pub(crate) struct ReusedRequest {
770        pub key: RequestKey,
771        pub output: RawOutput,
772        pub source_attempt_id: String,
773    }
774
775    /// By step and instance.
776    pub(crate) type ReusedRequests = BTreeMap<(String, Vec<String>), ReusedRequest>;
777
778    /// A decision entry of the memory tier (spec 044, 3.6.4). It holds the
779    /// reusable work and its lineage, never a judgment to serve.
780    #[derive(Debug, Clone)]
781    pub(crate) struct DecisionEntry {
782        pub requests: ReusedRequests,
783        pub source_run_record: ContentDigest,
784        /// The source decision's evaluation time: decision entries expire by
785        /// evaluation time (spec 044, decision 6).
786        pub created: Timestamp,
787    }
788
789    /// A retained decision entry, in its namespace's state.
790    #[derive(Debug, Clone)]
791    pub(super) struct DecisionSlot {
792        pub(super) key: RequestKey,
793        entry: Arc<DecisionEntry>,
794        entry_id: String,
795        generation: u64,
796        pub(super) bytes: u64,
797        seq: u64,
798    }
799
800    /// A decision entry that may be served.
801    #[derive(Debug, Clone)]
802    pub(crate) struct DecisionHit {
803        pub entry: Arc<DecisionEntry>,
804        pub entry_id: String,
805        pub age_ms: u64,
806        /// The store's generations, for an entry of the persistent tier.
807        pub generations: Option<EntryGenerations>,
808    }
809
810    /// An entry's id: a digest of its key material and its source run
811    /// record's digest, the same in either tier.
812    pub(crate) fn decision_entry_id(key: &RequestKey, source: &ContentDigest) -> String {
813        let mut material = key.material.clone();
814        part(&mut material, source.as_str().as_bytes());
815        tagged_digest(DECISION_ENTRY_TAG, &material)
816    }
817
818    /// Why a decision lookup did not hit; each is a diagnostic (spec 044, 3.9).
819    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
820    pub(crate) enum DecisionMiss {
821        Absent,
822        Collision,
823        GenerationAdvanced,
824        FromFuture,
825        Expired,
826    }
827
828    impl DecisionMiss {
829        pub(crate) fn diagnostic(self) -> &'static str {
830            match self {
831                DecisionMiss::Absent => "decision cache miss",
832                DecisionMiss::Collision => "entry_collision",
833                DecisionMiss::GenerationAdvanced => "generation_advanced",
834                DecisionMiss::FromFuture => "entry_from_future",
835                DecisionMiss::Expired => "entry_expired",
836            }
837        }
838    }
839
840    /// The bytes a decision entry counts against `max_entry_bytes`: its key
841    /// material, and each reused request's key material, canonical output and
842    /// source attempt id.
843    pub(crate) fn decision_entry_bytes(
844        key: &RequestKey,
845        requests: &ReusedRequests,
846    ) -> Result<u64, CacheRefusal> {
847        let mut bytes = key.material.len() as u64;
848        for r in requests.values() {
849            let output =
850                record_canonical_bytes(&r.output).map_err(|_| CacheRefusal::Uncanonical)?;
851            bytes = bytes
852                .saturating_add(r.key.material.len() as u64)
853                .saturating_add(output.len() as u64)
854                .saturating_add(r.source_attempt_id.len() as u64);
855        }
856        Ok(bytes)
857    }
858
859    impl Optimizer {
860        /// Read the decision key's entry (spec 044, 3.6.3 item 1). An entry is
861        /// served only with the request's own key material, the namespace's
862        /// current generation, and an age by evaluation time within `ttl_ms`.
863        pub(crate) fn lookup_decision(
864            &self,
865            key: &RequestKey,
866            now: Timestamp,
867            ttl_ms: u64,
868        ) -> Result<DecisionHit, DecisionMiss> {
869            let mut namespaces = self.lock();
870            let Some(ns) = namespaces.get_mut(&key.namespace) else {
871                return Err(DecisionMiss::Absent);
872            };
873            let Some(slot) = ns.decisions.get(&key.id) else {
874                return Err(DecisionMiss::Absent);
875            };
876            if slot.key.material != key.material {
877                return Err(DecisionMiss::Collision);
878            }
879            if slot.generation != ns.generation {
880                ns.decisions.remove(&key.id);
881                return Err(DecisionMiss::GenerationAdvanced);
882            }
883            let (now, created) = (now.as_ms(), slot.entry.created.as_ms());
884            if now < created {
885                return Err(DecisionMiss::FromFuture);
886            }
887            let age_ms = u64::try_from(now - created).unwrap_or(u64::MAX);
888            if age_ms > ttl_ms {
889                ns.decisions.remove(&key.id);
890                return Err(DecisionMiss::Expired);
891            }
892            Ok(DecisionHit {
893                entry: slot.entry.clone(),
894                entry_id: slot.entry_id.clone(),
895                age_ms,
896                generations: None,
897            })
898        }
899
900        /// Admit a decision entry under the generation captured when the
901        /// decision started (spec 044, 3.6.4), evicting the namespace's oldest
902        /// decision entries beyond `max_entries`.
903        pub(crate) fn insert_decision(
904            &self,
905            policy: &DecisionCachePolicy,
906            key: RequestKey,
907            entry: DecisionEntry,
908            captured_generation: u64,
909        ) -> Result<String, CacheRefusal> {
910            let bytes = decision_entry_bytes(&key, &entry.requests)?;
911            if bytes > policy.max_entry_bytes {
912                return Err(CacheRefusal::EntryTooLarge);
913            }
914            // Present and positive exactly when the policy has a memory tier
915            // (spec 044, 3.1); otherwise refused, never bounded by a default.
916            let Some(max_entries) = policy.max_entries.filter(|&n| n > 0) else {
917                return Err(CacheRefusal::NoCapacity);
918            };
919            let mut namespaces = self.lock();
920            let ns = namespaces.entry(key.namespace.clone()).or_default();
921            if ns.generation != captured_generation {
922                return Err(CacheRefusal::StaleGeneration);
923            }
924            if ns
925                .decisions
926                .get(&key.id)
927                .is_some_and(|slot| slot.key.material != key.material)
928            {
929                return Err(CacheRefusal::Collision);
930            }
931            ns.decisions.remove(&key.id);
932            while ns.decisions.len() as u64 >= max_entries {
933                let Some((id, seq)) = ns.decision_fifo.pop_front() else {
934                    break;
935                };
936                if ns.decisions.get(&id).is_some_and(|slot| slot.seq == seq) {
937                    ns.decisions.remove(&id);
938                }
939            }
940            if ns.decision_fifo.len() > 2 * ns.decisions.len() + 16 {
941                let decisions = &ns.decisions;
942                ns.decision_fifo
943                    .retain(|(id, seq)| decisions.get(id).is_some_and(|slot| slot.seq == *seq));
944            }
945            let entry_id = decision_entry_id(&key, &entry.source_run_record);
946            let seq = ns.next_seq;
947            ns.next_seq += 1;
948            ns.decision_fifo.push_back((key.id.clone(), seq));
949            let generation = ns.generation;
950            ns.decisions.insert(
951                key.id.clone(),
952                DecisionSlot {
953                    key,
954                    entry: Arc::new(entry),
955                    entry_id: entry_id.clone(),
956                    generation,
957                    bytes,
958                    seq,
959                },
960            );
961            Ok(entry_id)
962        }
963    }
964
965    type At = (String, Vec<String>);
966
967    /// What one decision did with the decision cache, filled while it runs.
968    #[derive(Debug, Default)]
969    pub(crate) struct DecisionLog {
970        /// The 017 key of each request outside the closure, on a miss.
971        pub keys: BTreeMap<At, RequestKey>,
972        /// Each output supplied to the core, on a miss.
973        pub outputs: BTreeMap<At, RawOutput>,
974        pub reused: Vec<At>,
975        pub refreshed: Vec<At>,
976        pub mismatched: Vec<At>,
977        pub diagnostics: Vec<String>,
978    }
979
980    /// One decision's part in the decision-result cache (spec 044, 3.6): its
981    /// key, the entry it is served from on a hit, and its log.
982    #[derive(Debug)]
983    pub(crate) struct DecisionRun {
984        /// Steps whose requests are dispatched again on a hit (3.6.2).
985        pub closure: BTreeSet<String>,
986        /// Absent when the decision is unkeyed (3.5 item 3) or live.
987        pub key: Option<RequestKey>,
988        /// The namespace generation captured before the lookup; admission
989        /// uses it (3.6.4).
990        pub generation: u64,
991        /// The store's generations, captured by the persistent read before
992        /// the work began; a write needs them (3.4.3).
993        pub captured: Option<EntryGenerations>,
994        pub hit: Option<DecisionHit>,
995        /// Under `admission: refuse`, an unkeyed decision's requests are
996        /// refused with this diagnostic and never dispatched.
997        pub refused: Option<String>,
998        outcome: rustev_contract::optimization::DecisionCacheOutcome,
999        log: Mutex<DecisionLog>,
1000    }
1001
1002    impl DecisionRun {
1003        /// Derive the key and read the memory tier after admission and before
1004        /// the core starts (spec 044, 3.6.3 item 1); [`DecisionRun::read_store`]
1005        /// reads the persistent tier next. `None` for a plan without the
1006        /// decision cache.
1007        pub(crate) fn start(
1008            optimizer: &Optimizer,
1009            plan: &crate::PreparedPlan,
1010            req: &crate::DecisionRequest,
1011            dimensions: &BTreeMap<String, rustev_contract::scope::Handle>,
1012            live: bool,
1013        ) -> Option<DecisionRun> {
1014            use rustev_contract::optimization::{DecisionCacheOutcome, OptimizationAdmission};
1015            use rustev_contract::plan::PlanExecution;
1016            let policy = plan.optimization.as_ref()?;
1017            let cache = policy.decision_cache.as_ref()?;
1018            let PlanExecution::Declared(d) = &plan.compiled.plan.execution else {
1019                return None;
1020            };
1021            let compiled = d.decision_cache.as_ref()?;
1022            let mut run = DecisionRun {
1023                closure: compiled.refresh_closure.iter().cloned().collect(),
1024                key: None,
1025                generation: 0,
1026                captured: None,
1027                hit: None,
1028                refused: None,
1029                outcome: DecisionCacheOutcome::Miss,
1030                log: Mutex::default(),
1031            };
1032            let log = run.log.get_mut().unwrap_or_else(|e| e.into_inner());
1033            // A live re-execution observes fresh backend work (spec 026, 3.4).
1034            if live {
1035                run.outcome = DecisionCacheOutcome::Bypassed;
1036                log.diagnostics
1037                    .push("live re-execution observes fresh backend work".into());
1038                return Some(run);
1039            }
1040            let key = decision_key(DecisionKeyParts {
1041                schema: &compiled.decision_key_schema,
1042                plan_id: &plan.compiled.id,
1043                profile: &profile_of(&plan.compiled.plan),
1044                policy,
1045                principal: &req.principal_handle,
1046                dimensions,
1047                snapshot: &req.snapshot,
1048            });
1049            match key {
1050                Err(unkeyed) => {
1051                    let diagnostic = unkeyed.diagnostic();
1052                    log.diagnostics.push(diagnostic.clone());
1053                    if policy.admission == OptimizationAdmission::Refuse {
1054                        run.outcome = DecisionCacheOutcome::Refused;
1055                        run.refused = Some(diagnostic);
1056                    } else {
1057                        run.outcome = DecisionCacheOutcome::Bypassed;
1058                    }
1059                }
1060                Ok(key) => {
1061                    run.generation = optimizer.generation(&key.namespace);
1062                    if cache.tiers.contains(&DecisionCacheTier::Memory) {
1063                        match optimizer.lookup_decision(&key, req.evaluation_time, cache.ttl_ms) {
1064                            Ok(hit) => run.hit = Some(hit),
1065                            Err(miss) => log.diagnostics.push(miss.diagnostic().into()),
1066                        }
1067                    }
1068                    run.key = Some(key);
1069                }
1070            }
1071            Some(run)
1072        }
1073
1074        /// After a memory miss, the persistent tier (spec 044, 3.3). A served
1075        /// entry is admitted to the memory tier under its bounds. A store
1076        /// failure under `fail_closed` leaves the decision neither served
1077        /// nor admitted, `refused` or `bypassed` under `admission`; under
1078        /// `continue_without_cache` it is a miss (3.4.2 and 3.6.6).
1079        pub(crate) async fn read_store(
1080            &mut self,
1081            optimizer: &Optimizer,
1082            policy: &OptimizationPolicy,
1083            cache: &DecisionCachePolicy,
1084            calls: &crate::store::StoreCtx<'_>,
1085            now: Timestamp,
1086        ) {
1087            use crate::store::{DecisionRead, read_decision};
1088            use rustev_contract::optimization::{
1089                DecisionCacheOutcome, OptimizationAdmission, StoreFailurePolicy,
1090            };
1091            if !cache.tiers.contains(&DecisionCacheTier::Persistent)
1092                || self.hit.is_some()
1093                || self.refused.is_some()
1094            {
1095                return;
1096            }
1097            let Some(key) = self.key.clone() else {
1098                return;
1099            };
1100            match read_decision(calls, &key, now).await {
1101                DecisionRead::Hit {
1102                    entry,
1103                    age_ms,
1104                    generations,
1105                } => {
1106                    let entry_id = decision_entry_id(&key, &entry.source_run_record);
1107                    let entry = Arc::new(entry);
1108                    if cache.tiers.contains(&DecisionCacheTier::Memory)
1109                        && let Err(refusal) =
1110                            optimizer.insert_decision(cache, key, (*entry).clone(), self.generation)
1111                    {
1112                        self.diagnose(crate::driver::cache_refusal_detail(refusal).into());
1113                    }
1114                    self.hit = Some(DecisionHit {
1115                        entry,
1116                        entry_id,
1117                        age_ms,
1118                        generations: Some(generations),
1119                    });
1120                }
1121                DecisionRead::Miss {
1122                    captured,
1123                    diagnostic,
1124                } => {
1125                    self.captured = captured;
1126                    self.diagnose(diagnostic);
1127                }
1128                DecisionRead::Failed(detail) => {
1129                    self.diagnose(detail.clone());
1130                    if calls.persistent.on_store_failure == StoreFailurePolicy::FailClosed {
1131                        self.key = None;
1132                        if policy.admission == OptimizationAdmission::Refuse {
1133                            self.outcome = DecisionCacheOutcome::Refused;
1134                            self.refused = Some(detail);
1135                        } else {
1136                            self.outcome = DecisionCacheOutcome::Bypassed;
1137                        }
1138                    }
1139                }
1140            }
1141        }
1142
1143        pub(crate) fn log(&self) -> MutexGuard<'_, DecisionLog> {
1144            self.log.lock().unwrap_or_else(|e| e.into_inner())
1145        }
1146
1147        /// The run record member (spec 044, 3.6.6). `unresolved_volatile` names
1148        /// each volatile input the core did not accept from the new snapshot; it
1149        /// and any refreshed request without an output are a failed refresh,
1150        /// supplied as it is under `unresolved` (3.6.5).
1151        pub(crate) fn record(
1152            &self,
1153            requests: &[rustev_contract::run::RequestRecord],
1154            unresolved_volatile: &[String],
1155        ) -> rustev_contract::optimization::DecisionCacheRecord {
1156            use rustev_contract::optimization::{
1157                DecisionCacheOutcome, DecisionCacheRecord, MAX_OPTIMIZATION_DIAGNOSTICS,
1158                RefreshFailurePolicy,
1159            };
1160            use rustev_contract::run::RequestResult;
1161            let log = self.log();
1162            let index = |list: &[At]| {
1163                let mut found: Vec<u32> = list
1164                    .iter()
1165                    .filter_map(|(step, instance)| {
1166                        requests
1167                            .iter()
1168                            .position(|r| &r.step == step && &r.instance == instance)
1169                    })
1170                    .map(|n| n as u32)
1171                    .collect();
1172                found.sort_unstable();
1173                found
1174            };
1175            let mut record = DecisionCacheRecord {
1176                outcome: self.outcome,
1177                entry_id: None,
1178                source_run_record: None,
1179                age_ms: None,
1180                generations: None,
1181                reused: index(&log.reused),
1182                refreshed: index(&log.refreshed),
1183                mismatched: index(&log.mismatched),
1184                substituted: vec![],
1185                on_refresh_failure: None,
1186                diagnostics: vec![],
1187            };
1188            let mut diagnostics = log.diagnostics.clone();
1189            if let Some(hit) = &self.hit {
1190                record.outcome = if record.refreshed.is_empty() && record.mismatched.is_empty() {
1191                    DecisionCacheOutcome::Hit
1192                } else {
1193                    DecisionCacheOutcome::Refreshed
1194                };
1195                record.entry_id = Some(hit.entry_id.clone());
1196                record.source_run_record = Some(hit.entry.source_run_record.clone());
1197                record.age_ms = Some(hit.age_ms);
1198                record.generations = hit.generations;
1199                let mut failed: Vec<String> = unresolved_volatile
1200                    .iter()
1201                    .map(|v| format!("input {v:?}"))
1202                    .collect();
1203                failed.extend(record.refreshed.iter().filter_map(|&n| {
1204                    let r = &requests[n as usize];
1205                    matches!(
1206                        r.result,
1207                        RequestResult::Failed(_) | RequestResult::NotSupplied
1208                    )
1209                    .then(|| format!("step {:?}", r.step))
1210                }));
1211                if !failed.is_empty() {
1212                    record.on_refresh_failure = Some(RefreshFailurePolicy::Unresolved);
1213                    diagnostics.push(format!("refresh_failed: {}", failed.join(", ")));
1214                }
1215            }
1216            for d in diagnostics {
1217                let d = crate::wait::cut(&d, crate::driver::DETAIL_BYTES);
1218                match record.diagnostics.len() {
1219                    n if n + 1 < MAX_OPTIMIZATION_DIAGNOSTICS => record.diagnostics.push(d),
1220                    n if n + 1 == MAX_OPTIMIZATION_DIAGNOSTICS => {
1221                        record
1222                            .diagnostics
1223                            .push("further diagnostics dropped".into());
1224                    }
1225                    _ => {}
1226                }
1227            }
1228            record
1229        }
1230
1231        /// The entry a judged miss may admit (spec 044, 3.6.4): every request
1232        /// outside the closure was supplied a validated output of target 0
1233        /// without a fallback transition, and the entry fits
1234        /// `max_entry_bytes`. A served, unkeyed or live decision admits
1235        /// nothing. `Err` holds the diagnostic of an admission not made.
1236        pub(crate) fn admissible(
1237            &self,
1238            requests: &[rustev_contract::run::RequestRecord],
1239            policy: &rustev_contract::optimization::DecisionCachePolicy,
1240        ) -> Result<Option<(RequestKey, ReusedRequests)>, String> {
1241            use rustev_contract::run::{RequestResult, Transition};
1242            let (Some(key), None) = (&self.key, &self.hit) else {
1243                return Ok(None);
1244            };
1245            let log = self.log();
1246            let mut reused = ReusedRequests::new();
1247            for r in requests.iter().filter(|r| !self.closure.contains(&r.step)) {
1248                let at = (r.step.clone(), r.instance.clone());
1249                let fallback = r
1250                    .transitions
1251                    .iter()
1252                    .any(|t| matches!(t, Transition::Fallback { .. }));
1253                let source = r
1254                    .supplied_from()
1255                    .filter(|f| f.target == 0 && matches!(r.result, RequestResult::Output { .. }))
1256                    .and_then(|f| {
1257                        f.attempt
1258                            .map(|a| a.attempt_id.clone())
1259                            .or(f.reused_from.map(String::from))
1260                    });
1261                match (fallback, source, log.keys.get(&at), log.outputs.get(&at)) {
1262                    (false, Some(source_attempt_id), Some(k), Some(output)) => {
1263                        reused.insert(
1264                            at,
1265                            ReusedRequest {
1266                                key: k.clone(),
1267                                output: output.clone(),
1268                                source_attempt_id,
1269                            },
1270                        );
1271                    }
1272                    _ => {
1273                        return Err(format!(
1274                            "decision cache not admitted: step {:?} has no validated target 0 output",
1275                            r.step
1276                        ));
1277                    }
1278                }
1279            }
1280            match decision_entry_bytes(key, &reused) {
1281                Ok(bytes) if bytes <= policy.max_entry_bytes => Ok(Some((key.clone(), reused))),
1282                Ok(_) | Err(CacheRefusal::EntryTooLarge) => {
1283                    Err("decision cache not admitted: entry exceeds max_entry_bytes".into())
1284                }
1285                Err(_) => {
1286                    Err("decision cache not admitted: an output has no canonical form".into())
1287                }
1288            }
1289        }
1290
1291        /// Record that this decision's evidence carries an admission not made.
1292        pub(crate) fn diagnose(&self, diagnostic: String) {
1293            self.log().diagnostics.push(diagnostic);
1294        }
1295    }
1296
1297    #[cfg(test)]
1298    mod tests {
1299        use rustev_contract::definition::ProvenanceClass;
1300        use rustev_contract::optimization::{
1301            DecisionCacheTier, ExpiryBasis, OptimizationAdmission, RefreshFailurePolicy,
1302        };
1303        use rustev_contract::snapshot::Entry;
1304
1305        use super::super::InvalidationSelector;
1306        use super::*;
1307
1308        fn plan() -> (PlanId, OptimizationPolicy) {
1309            (
1310                PlanId::parse(&format!("sha256:{}", "1".repeat(64))).unwrap(),
1311                OptimizationPolicy {
1312                    key_schema_version: 1,
1313                    cache_namespace_version: 1,
1314                    admission: OptimizationAdmission::Refuse,
1315                    expiry: ExpiryBasis::InjectedRuntimeTime,
1316                    batch: None,
1317                    memory_cache: None,
1318                    shared_calls: None,
1319                    persistent_cache: None,
1320                    decision_cache: None,
1321                },
1322            )
1323        }
1324
1325        fn cache(max_entries: u64, max_entry_bytes: u64) -> DecisionCachePolicy {
1326            DecisionCachePolicy {
1327                tiers: vec![DecisionCacheTier::Memory],
1328                max_entry_bytes,
1329                max_entries: Some(max_entries),
1330                ttl_ms: 100,
1331                volatile: vec!["live".into()],
1332                key_dimensions: vec!["revision".into()],
1333                on_refresh_failure: RefreshFailurePolicy::Unresolved,
1334            }
1335        }
1336
1337        /// The schema the compiler writes for `cache()` over the inputs
1338        /// `stable` and `live` (spec 044, 3.6.1).
1339        fn schema() -> Vec<String> {
1340            [
1341                "plan_id",
1342                "key_schema_version",
1343                "cache_namespace_version",
1344                "runtime_version",
1345                "scope",
1346                "profile",
1347                "dimension:revision",
1348                "input:stable",
1349            ]
1350            .map(String::from)
1351            .to_vec()
1352        }
1353
1354        fn snapshot(stable: i64, live: i64) -> Snapshot {
1355            let entry = |field: &str, value: i64| Entry {
1356                field: field.into(),
1357                value: value.into(),
1358                provenance: ProvenanceClass::SystemOfRecord,
1359                as_of_ms: Timestamp::from_ms(1).unwrap(),
1360                source: "SYNTHETIC".into(),
1361                grade: None,
1362                producer: None,
1363            };
1364            Snapshot {
1365                schema: rustev_contract::schema::SNAPSHOT.into(),
1366                entries: vec![entry("stable", stable), entry("live", live)],
1367            }
1368        }
1369
1370        fn dims(revision: &str) -> BTreeMap<String, Handle> {
1371            BTreeMap::from([("revision".to_string(), Handle::new(revision).unwrap())])
1372        }
1373
1374        fn key_with(
1375            schema: &[String],
1376            profile: &str,
1377            principal: &[u8],
1378            dimensions: &BTreeMap<String, Handle>,
1379            snapshot: &Snapshot,
1380        ) -> Result<RequestKey, Unkeyed> {
1381            let (plan_id, policy) = plan();
1382            decision_key(DecisionKeyParts {
1383                schema,
1384                plan_id: &plan_id,
1385                profile,
1386                policy: &policy,
1387                principal,
1388                dimensions,
1389                snapshot,
1390            })
1391        }
1392
1393        fn key(principal: &[u8]) -> RequestKey {
1394            key_with(&schema(), "none", principal, &dims("r1"), &snapshot(1, 1)).unwrap()
1395        }
1396
1397        fn at(ms: i64) -> Timestamp {
1398            Timestamp::from_ms(ms).unwrap()
1399        }
1400
1401        /// An entry reusing one request, created at `created`.
1402        fn entry(created: i64, output_bytes: usize) -> DecisionEntry {
1403            let reused = ReusedRequest {
1404                key: key(b"request"),
1405                output: RawOutput::Label("x".repeat(output_bytes)),
1406                source_attempt_id: "d1/topic/0/1".into(),
1407            };
1408            DecisionEntry {
1409                requests: BTreeMap::from([(("topic".to_string(), vec![]), reused)]),
1410                source_run_record: ContentDigest::parse(&format!("sha256:{}", "2".repeat(64)))
1411                    .unwrap(),
1412                created: at(created),
1413            }
1414        }
1415
1416        /// Spec 044, 3.5 and 3.6.1: the decision key follows the compiled
1417        /// schema. The profile, scope, each dimension and each listed input's
1418        /// snapshot entries are members; an input the schema leaves out (a
1419        /// volatile one) is not. A missing dimension, an undeclared one and
1420        /// an unknown member leave the decision unkeyed.
1421        #[test]
1422        fn the_decision_key_follows_its_schema() {
1423            let (plan_id, policy) = plan();
1424            let s = schema();
1425            let base = key(b"tenant-a");
1426            assert_eq!(base.namespace, namespace(&plan_id, &policy));
1427            assert_eq!(base.scope_id, scope_id(b"tenant-a"));
1428            // A volatile input is not a member: a changed live value keeps
1429            // the key.
1430            assert_eq!(
1431                key_with(&s, "none", b"tenant-a", &dims("r1"), &snapshot(1, 2)).unwrap(),
1432                base
1433            );
1434            for other in [
1435                key_with(&s, r#"["p"]"#, b"tenant-a", &dims("r1"), &snapshot(1, 1)),
1436                key_with(&s, "none", b"tenant-b", &dims("r1"), &snapshot(1, 1)),
1437                key_with(&s, "none", b"tenant-a", &dims("r2"), &snapshot(1, 1)),
1438                key_with(&s, "none", b"tenant-a", &dims("r1"), &snapshot(2, 1)),
1439            ] {
1440                assert_ne!(other.unwrap().id, base.id);
1441            }
1442            let missing = key_with(&s, "none", b"tenant-a", &BTreeMap::new(), &snapshot(1, 1));
1443            assert_eq!(
1444                missing.unwrap_err().diagnostic(),
1445                "key_dimension_missing: \"revision\""
1446            );
1447            let mut extra = dims("r1");
1448            extra.insert("region".into(), Handle::new("eu").unwrap());
1449            assert_eq!(
1450                key_with(&s, "none", b"tenant-a", &extra, &snapshot(1, 1))
1451                    .unwrap_err()
1452                    .diagnostic(),
1453                "key_ineligible: dimension \"region\" is not declared by the plan"
1454            );
1455            let mut unknown = s.clone();
1456            unknown.push("clock".into());
1457            assert_eq!(
1458                key_with(&unknown, "none", b"tenant-a", &dims("r1"), &snapshot(1, 1))
1459                    .unwrap_err()
1460                    .diagnostic(),
1461                "key_ineligible: unknown key member \"clock\""
1462            );
1463        }
1464
1465        /// Spec 044, decision 6: a decision entry is served by evaluation
1466        /// time up to `ttl_ms`; one created after the request's evaluation
1467        /// time is a miss and kept; an expired one is a miss and removed.
1468        #[test]
1469        fn a_decision_entry_expires_by_evaluation_time() {
1470            let o = Optimizer::default();
1471            let k = key(b"tenant-a");
1472            let id = o
1473                .insert_decision(&cache(4, 4_096), k.clone(), entry(1_000, 1), 0)
1474                .unwrap();
1475            let hit = o.lookup_decision(&k, at(1_005), 100).unwrap();
1476            assert_eq!((hit.entry_id.as_str(), hit.age_ms), (id.as_str(), 5));
1477            assert_eq!(hit.entry.requests.len(), 1);
1478            assert_eq!(o.lookup_decision(&k, at(1_100), 100).unwrap().age_ms, 100);
1479            assert_eq!(
1480                o.lookup_decision(&k, at(999), 100).unwrap_err(),
1481                DecisionMiss::FromFuture
1482            );
1483            assert!(o.lookup_decision(&k, at(1_000), 100).is_ok(), "kept");
1484            assert_eq!(
1485                o.lookup_decision(&k, at(1_101), 100).unwrap_err(),
1486                DecisionMiss::Expired
1487            );
1488            assert_eq!(
1489                o.lookup_decision(&k, at(1_000), 100).unwrap_err(),
1490                DecisionMiss::Absent,
1491                "removed"
1492            );
1493            // The entry id binds the key and the source record, and each
1494            // miss names its diagnostic (spec 044, 3.9).
1495            assert_ne!(id, k.id);
1496            let codes = [
1497                DecisionMiss::Absent,
1498                DecisionMiss::Collision,
1499                DecisionMiss::GenerationAdvanced,
1500                DecisionMiss::FromFuture,
1501                DecisionMiss::Expired,
1502            ]
1503            .map(DecisionMiss::diagnostic);
1504            assert_eq!(
1505                codes,
1506                [
1507                    "decision cache miss",
1508                    "entry_collision",
1509                    "generation_advanced",
1510                    "entry_from_future",
1511                    "entry_expired"
1512                ]
1513            );
1514        }
1515
1516        /// Spec 044, 3.6.4 and 017 3.2: admission refuses an entry over
1517        /// `max_entry_bytes`, one captured under an earlier generation and
1518        /// one whose key id holds other material; a lookup with other
1519        /// material is a collision; the oldest entry goes beyond
1520        /// `max_entries`.
1521        #[test]
1522        fn admission_is_bounded_and_checked() {
1523            let o = Optimizer::default();
1524            let a = key(b"tenant-a");
1525            let small = decision_entry_bytes(&a, &entry(0, 1).requests).unwrap();
1526            let policy = cache(2, small);
1527            assert_eq!(
1528                o.insert_decision(&policy, a.clone(), entry(0, 2), 0),
1529                Err(CacheRefusal::EntryTooLarge)
1530            );
1531            for max_entries in [None, Some(0)] {
1532                let empty = DecisionCachePolicy {
1533                    max_entries,
1534                    ..policy.clone()
1535                };
1536                assert_eq!(
1537                    o.insert_decision(&empty, a.clone(), entry(0, 1), 0),
1538                    Err(CacheRefusal::NoCapacity)
1539                );
1540            }
1541            o.insert_decision(&policy, a.clone(), entry(0, 1), 0)
1542                .unwrap();
1543            assert_eq!(
1544                o.insert_decision(&policy, a.clone(), entry(0, 1), 1),
1545                Err(CacheRefusal::StaleGeneration)
1546            );
1547            let mut other = a.clone();
1548            *other.material.last_mut().unwrap() ^= 1;
1549            assert_eq!(
1550                o.insert_decision(&policy, other.clone(), entry(0, 1), 0),
1551                Err(CacheRefusal::Collision)
1552            );
1553            assert_eq!(
1554                o.lookup_decision(&other, at(0), 100).unwrap_err(),
1555                DecisionMiss::Collision
1556            );
1557            // Replacing an entry under the same key is not an eviction.
1558            o.insert_decision(&policy, a.clone(), entry(0, 1), 0)
1559                .unwrap();
1560            let (b, c) = (key(b"tenant-b"), key(b"tenant-c"));
1561            o.insert_decision(&policy, b.clone(), entry(0, 1), 0)
1562                .unwrap();
1563            o.insert_decision(&policy, c.clone(), entry(0, 1), 0)
1564                .unwrap();
1565            assert_eq!(
1566                o.lookup_decision(&a, at(0), 100).unwrap_err(),
1567                DecisionMiss::Absent
1568            );
1569            assert!(o.lookup_decision(&b, at(0), 100).is_ok());
1570            assert!(o.lookup_decision(&c, at(0), 100).is_ok());
1571        }
1572
1573        /// Spec 017, 3.7: invalidating a scope removes its decision entries
1574        /// and counts them; the generation advances for the namespace, so
1575        /// no earlier entry is served and no earlier capture is admitted.
1576        #[test]
1577        fn invalidation_reaches_decision_entries() {
1578            let o = Optimizer::default();
1579            let (a, b) = (key(b"tenant-a"), key(b"tenant-b"));
1580            let policy = cache(4, 4_096);
1581            o.insert_decision(&policy, a.clone(), entry(0, 1), 0)
1582                .unwrap();
1583            o.insert_decision(&policy, b.clone(), entry(0, 1), 0)
1584                .unwrap();
1585            let bytes = decision_entry_bytes(&a, &entry(0, 1).requests).unwrap();
1586            let result = o.invalidate(&InvalidationSelector::Scope {
1587                namespace: a.namespace.clone(),
1588                scope_id: a.scope_id.clone(),
1589            });
1590            assert_eq!((result.removed_entries, result.removed_bytes), (1, bytes));
1591            assert_eq!(
1592                o.lookup_decision(&a, at(0), 100).unwrap_err(),
1593                DecisionMiss::Absent
1594            );
1595            assert_eq!(
1596                o.lookup_decision(&b, at(0), 100).unwrap_err(),
1597                DecisionMiss::GenerationAdvanced
1598            );
1599            assert_eq!(
1600                o.insert_decision(&policy, a.clone(), entry(0, 1), 0),
1601                Err(CacheRefusal::StaleGeneration)
1602            );
1603            o.insert_decision(&policy, a.clone(), entry(0, 1), result.generation)
1604                .unwrap();
1605            let all = o.invalidate(&InvalidationSelector::Namespace {
1606                namespace: a.namespace.clone(),
1607            });
1608            assert_eq!(all.removed_entries, 1);
1609        }
1610    }
1611}
1612
1613#[cfg(test)]
1614mod tests {
1615    use std::collections::BTreeMap;
1616
1617    use rustev_contract::ids::{ArtifactId, DescriptorId, PlanId};
1618    use rustev_contract::optimization::{EvictionOrder, ExpiryBasis, OptimizationAdmission};
1619
1620    use super::*;
1621
1622    fn digest(byte: char) -> String {
1623        format!("sha256:{}", byte.to_string().repeat(64))
1624    }
1625
1626    fn policy(plan: char) -> (PlanId, OptimizationPolicy) {
1627        (
1628            PlanId::parse(&digest(plan)).unwrap(),
1629            OptimizationPolicy {
1630                key_schema_version: 1,
1631                cache_namespace_version: 1,
1632                admission: OptimizationAdmission::Refuse,
1633                expiry: ExpiryBasis::InjectedRuntimeTime,
1634                batch: None,
1635                memory_cache: Some(memory(4096, 2, 2048, 2048)),
1636                shared_calls: None,
1637                persistent_cache: None,
1638                decision_cache: None,
1639            },
1640        )
1641    }
1642
1643    fn memory(max_bytes: u64, max_entries: u64, entry: u64, scope: u64) -> MemoryCachePolicy {
1644        MemoryCachePolicy {
1645            acquisition_pages: false,
1646            max_bytes,
1647            max_entries,
1648            max_entry_bytes: entry,
1649            max_scope_bytes: scope,
1650            ttl_ms: 10,
1651            eviction: EvictionOrder::Fifo,
1652        }
1653    }
1654
1655    fn key_in(plan: char, principal: &[u8], step: &str) -> RequestKey {
1656        let (plan_id, policy) = policy(plan);
1657        request_key(KeyParts {
1658            plan_id: &plan_id,
1659            profile: "none",
1660            policy: &policy,
1661            principal,
1662            backend_artifact: &ArtifactId::parse(&digest('2')).unwrap(),
1663            backend_descriptor: &DescriptorId::parse(&digest('3')).unwrap(),
1664            step,
1665            instance: &["one".into()],
1666            projection: b"projection",
1667        })
1668    }
1669
1670    fn key(principal: &[u8]) -> RequestKey {
1671        key_in('1', principal, "classify")
1672    }
1673
1674    fn output(label: &str) -> RawOutput {
1675        RawOutput::Scores(BTreeMap::from([(label.into(), 1.0)]))
1676    }
1677
1678    fn write(attempt: &str, generation: u64) -> CacheWrite {
1679        CacheWrite {
1680            output: output(&"a".repeat(700)),
1681            source_attempt_id: attempt.into(),
1682            target: 0,
1683            captured_generation: generation,
1684            now: 0,
1685        }
1686    }
1687
1688    /// The accounted size of one `write("x", ..)` entry under `key`.
1689    fn size(key: &RequestKey) -> u64 {
1690        let optimizer = Optimizer::default();
1691        let roomy = memory(1 << 20, 64, 1 << 20, 1 << 20);
1692        optimizer
1693            .insert(&roomy, key.clone(), write("x", 0))
1694            .unwrap();
1695        optimizer.total_bytes(&key.namespace)
1696    }
1697
1698    #[test]
1699    fn scope_and_instance_arity_are_part_of_the_complete_key() {
1700        let a = key(b"tenant-a");
1701        let b = key(b"tenant-b");
1702        assert_ne!(a.id, b.id);
1703        assert_ne!(a.scope_id, b.scope_id);
1704        assert_ne!(a.material, b.material);
1705        // ["ab"] and ["a", "b"] are different instances.
1706        let (plan_id, policy) = policy('1');
1707        let with = |instance: &[String]| {
1708            request_key(KeyParts {
1709                plan_id: &plan_id,
1710                profile: "none",
1711                policy: &policy,
1712                principal: b"t",
1713                backend_artifact: &ArtifactId::parse(&digest('2')).unwrap(),
1714                backend_descriptor: &DescriptorId::parse(&digest('3')).unwrap(),
1715                step: "s",
1716                instance,
1717                projection: b"p",
1718            })
1719            .id
1720        };
1721        assert_ne!(with(&["ab".into()]), with(&["a".into(), "b".into()]));
1722    }
1723
1724    #[test]
1725    fn memory_cache_checks_expiry_collision_and_invalidation_generation() {
1726        let optimizer = Optimizer::default();
1727        let memory = policy('1').1.memory_cache.unwrap();
1728        let key = key(b"tenant");
1729        optimizer
1730            .insert(
1731                &memory,
1732                key.clone(),
1733                CacheWrite {
1734                    output: output("a"),
1735                    source_attempt_id: "attempt-1".into(),
1736                    target: 0,
1737                    captured_generation: 0,
1738                    now: 5,
1739                },
1740            )
1741            .unwrap();
1742        assert_eq!(optimizer.lookup(&key, 14).unwrap().age_ms, 9);
1743        assert_eq!(optimizer.lookup(&key, 15), Err(CacheMiss::Expired));
1744        assert_eq!(optimizer.lookup(&key, 15), Err(CacheMiss::Absent));
1745
1746        optimizer
1747            .insert(&memory, key.clone(), write("a-2", 0))
1748            .unwrap();
1749        optimizer.force_collision(&key, b"other material".to_vec());
1750        assert_eq!(optimizer.lookup(&key, 1), Err(CacheMiss::Collision));
1751        assert_eq!(
1752            optimizer.insert(&memory, key.clone(), write("a-3", 0)),
1753            Err(CacheRefusal::Collision)
1754        );
1755
1756        let result = optimizer.invalidate(&InvalidationSelector::Namespace {
1757            namespace: key.namespace.clone(),
1758        });
1759        assert_eq!(result.generation, 1);
1760        assert_eq!(result.removed_entries, 1);
1761        // A result captured before the invalidation is never stored and so
1762        // never served to a later request (I-3).
1763        assert_eq!(
1764            optimizer.insert(&memory, key.clone(), write("late", 0)),
1765            Err(CacheRefusal::StaleGeneration)
1766        );
1767        assert_eq!(optimizer.lookup(&key, 1), Err(CacheMiss::Absent));
1768        optimizer
1769            .insert(&memory, key.clone(), write("fresh", 1))
1770            .unwrap();
1771        assert_eq!(
1772            optimizer.lookup(&key, 1).unwrap().source_attempt_id,
1773            "fresh"
1774        );
1775    }
1776
1777    #[test]
1778    fn an_oversized_entry_is_refused_before_anything_is_evicted() {
1779        let optimizer = Optimizer::default();
1780        let a = key_in('1', b"tenant", "a");
1781        let n = size(&a);
1782        let tight = memory(4 * n, 2, n, 2 * n);
1783        optimizer.insert(&tight, a.clone(), write("a", 0)).unwrap();
1784        let big = CacheWrite {
1785            output: output(&"b".repeat(800)),
1786            ..write("b", 0)
1787        };
1788        assert_eq!(
1789            optimizer.insert(&tight, key_in('1', b"tenant", "b"), big),
1790            Err(CacheRefusal::EntryTooLarge)
1791        );
1792        assert!(optimizer.lookup(&a, 0).is_ok(), "nothing was evicted");
1793    }
1794
1795    #[test]
1796    fn total_and_count_bounds_evict_the_namespace_fifo_head() {
1797        let optimizer = Optimizer::default();
1798        let a = key(b"tenant-a");
1799        let n = size(&a);
1800        let total = memory(2 * n + n / 2, 8, n, 2 * n + n / 2);
1801        let b = key(b"tenant-b");
1802        let c = key(b"tenant-c");
1803        optimizer.insert(&total, a.clone(), write("a", 0)).unwrap();
1804        optimizer.insert(&total, b.clone(), write("b", 0)).unwrap();
1805        let admission = optimizer.insert(&total, c.clone(), write("c", 0)).unwrap();
1806        assert_eq!(admission.evicted_entries, 1);
1807        assert_eq!(optimizer.lookup(&a, 0), Err(CacheMiss::Absent));
1808        assert!(optimizer.lookup(&b, 0).is_ok());
1809        assert!(optimizer.lookup(&c, 0).is_ok());
1810
1811        let optimizer = Optimizer::default();
1812        let count = memory(100 * n, 2, n, 100 * n);
1813        optimizer.insert(&count, a.clone(), write("a", 0)).unwrap();
1814        optimizer.insert(&count, b.clone(), write("b", 0)).unwrap();
1815        let admission = optimizer.insert(&count, c.clone(), write("c", 0)).unwrap();
1816        assert_eq!(admission.evicted_entries, 1);
1817        assert_eq!(optimizer.lookup(&a, 0), Err(CacheMiss::Absent));
1818    }
1819
1820    #[test]
1821    fn a_scope_bound_evicts_only_that_scopes_entries() {
1822        let optimizer = Optimizer::default();
1823        // Tenant B's entry is the global FIFO head.
1824        let b = key_in('1', b"tenant-b", "b");
1825        let n = size(&b);
1826        // Step names and attempt ids differ by a byte or two.
1827        let scoped = memory(100 * n, 64, n + 8, 2 * n + n / 2);
1828        optimizer.insert(&scoped, b.clone(), write("b", 0)).unwrap();
1829        let a1 = key_in('1', b"tenant-a", "a1");
1830        let a2 = key_in('1', b"tenant-a", "a2");
1831        let a3 = key_in('1', b"tenant-a", "a3");
1832        optimizer
1833            .insert(&scoped, a1.clone(), write("a1", 0))
1834            .unwrap();
1835        optimizer
1836            .insert(&scoped, a2.clone(), write("a2", 0))
1837            .unwrap();
1838        let admission = optimizer
1839            .insert(&scoped, a3.clone(), write("a3", 0))
1840            .unwrap();
1841        assert_eq!(admission.evicted_entries, 1);
1842        assert_eq!(optimizer.lookup(&a1, 0), Err(CacheMiss::Absent));
1843        assert!(optimizer.lookup(&a2, 0).is_ok());
1844        assert!(optimizer.lookup(&a3, 0).is_ok());
1845        assert!(
1846            optimizer.lookup(&b, 0).is_ok(),
1847            "tenant A's traffic never evicts tenant B"
1848        );
1849    }
1850
1851    #[test]
1852    fn one_plans_bounds_never_evict_another_plans_entries() {
1853        let optimizer = Optimizer::default();
1854        let roomy = memory(100_000, 64, 2048, 100_000);
1855        let tiny = memory(2048, 1, 2048, 2048);
1856        let other = key_in('7', b"tenant", "x");
1857        optimizer
1858            .insert(&roomy, other.clone(), write("x", 0))
1859            .unwrap();
1860        for step in ["a", "b", "c"] {
1861            optimizer
1862                .insert(&tiny, key_in('1', b"tenant", step), write(step, 0))
1863                .unwrap();
1864        }
1865        assert!(optimizer.lookup(&other, 0).is_ok());
1866        // Invalidating one namespace leaves the other's generation alone.
1867        optimizer.invalidate(&InvalidationSelector::Namespace {
1868            namespace: key_in('1', b"tenant", "a").namespace,
1869        });
1870        assert_eq!(optimizer.generation(&other.namespace), 0);
1871        assert!(optimizer.lookup(&other, 0).is_ok());
1872    }
1873
1874    #[test]
1875    fn scope_invalidation_is_exact() {
1876        let optimizer = Optimizer::default();
1877        let roomy = memory(100_000, 64, 2048, 100_000);
1878        let a = key(b"tenant-a");
1879        let b = key(b"tenant-b");
1880        optimizer.insert(&roomy, a.clone(), write("a", 0)).unwrap();
1881        optimizer.insert(&roomy, b.clone(), write("b", 0)).unwrap();
1882        let result = optimizer.invalidate(&InvalidationSelector::Scope {
1883            namespace: a.namespace.clone(),
1884            scope_id: a.scope_id.clone(),
1885        });
1886        assert_eq!(result.removed_entries, 1);
1887        assert_eq!(optimizer.lookup(&a, 0), Err(CacheMiss::Absent));
1888        // The generation is namespace-wide, which is conservative: the other
1889        // scope's entry is no longer served.
1890        assert_eq!(optimizer.lookup(&b, 0), Err(CacheMiss::Expired));
1891    }
1892
1893    #[test]
1894    fn lazily_deleted_queue_slots_stay_bounded() {
1895        let optimizer = Optimizer::default();
1896        let roomy = memory(100_000, 64, 2048, 100_000);
1897        let key = key(b"tenant");
1898        for n in 0..500 {
1899            optimizer
1900                .insert(&roomy, key.clone(), write(&format!("a{n}"), 0))
1901                .unwrap();
1902        }
1903        let (fifo, scoped) = optimizer.queue_lengths(&key.namespace);
1904        assert!(fifo <= 20 && scoped <= 20, "{fifo} {scoped}");
1905    }
1906
1907    /// Spec 044, 3.5.2: the profile identity is its own key member, so a
1908    /// profile never shares a key with `none` or with another profile.
1909    #[test]
1910    fn the_profile_is_a_key_member() {
1911        let (plan_id, policy) = policy('1');
1912        let with = |profile: &str| {
1913            request_key(KeyParts {
1914                plan_id: &plan_id,
1915                profile,
1916                policy: &policy,
1917                principal: b"tenant-a",
1918                backend_artifact: &ArtifactId::parse(&digest('2')).unwrap(),
1919                backend_descriptor: &DescriptorId::parse(&digest('3')).unwrap(),
1920                step: "classify",
1921                instance: &["one".into()],
1922                projection: b"projection",
1923            })
1924        };
1925        let none = with("none");
1926        let a = with(&digest('a'));
1927        let b = with(&digest('b'));
1928        assert_eq!(none, key_in('1', b"tenant-a", "classify"));
1929        assert_ne!(none.id, a.id);
1930        assert_ne!(a.id, b.id);
1931        // A chain's ids are delimited: one id holding a comma is not two.
1932        assert_eq!(profile_ids(&["a", "b"]), r#"["a","b"]"#);
1933        assert_ne!(profile_ids(&["a,b"]), profile_ids(&["a", "b"]));
1934        assert_ne!(
1935            with(&profile_ids(&["a,b"])).id,
1936            with(&profile_ids(&["a", "b"])).id
1937        );
1938        // The namespace and scope are the plan's and the principal's alone.
1939        assert_eq!(
1940            (&a.namespace, &a.scope_id),
1941            (&none.namespace, &none.scope_id)
1942        );
1943    }
1944}