From 04caba6ebdc5da9cd37f7eb4cea701d04c026498 Mon Sep 17 00:00:00 2001 From: Test Date: Sat, 16 May 2026 22:28:25 -0500 Subject: [PATCH] =?UTF-8?q?feat(genome):=20demand-aligned-recall=20PR-3b?= =?UTF-8?q?=20=E2=80=94=20LocalDemandAlignedRecall=20ranking=20engine?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR-3b of demand-aligned-recall (GENOME-FOUNDRY-SENTINEL Part 7). Composes PR-3a's scoring function with a candidate-injection API to produce ranked RankedPools. PR-3c adds the working-set walker that sources candidates from the substrate; PR-3b stays pure ranking. What lands - CandidateArtifact — caller-provided candidate ready for scoring. Carries per-factor inputs (semantic, outcome, provenance) + residency + last-used timestamp. - LocalDemandAlignedRecall { weights, half_life_ms } — the ranking engine. Thread-safe through immutability. - new() / with_config(weights, half_life_ms) constructors. - rank(now_ms, candidates) — pure-function ranking: scores each via PR-3a's score(), partitions by PageKind into layers/experts/ engrams, sorts each sub-pool descending by RecallScore.combined, returns populated RankedPool. - weights() + half_life_ms() inspectors. Design choices - now_ms passed in (not SystemTime::now). Replay determinism is mandatory per spec; reading now() would break RecallTrace replay. - KVCache candidates silently dropped — spec's RankedPool has three sub-pools (layers/experts/engrams); KV cache is working-set state. - NaN-safe sort via partial_cmp + Ordering::Equal fallback. - trace_ref = Uuid::from_u128(now_ms) — deterministic placeholder; PR-3c replaces with richer RecallTrace. What is deliberately deferred (PR-3c) - DemandAlignedRecall trait impl (needs working-set + genome catalog sourcing) - Federation sourcing (RecallScope::Federation / LocalThenGrid) - RecallTrace replay backing store (separate sentinel PR) - Embedding model integration Tests 13 new tests pin the ranking behavior: - new + with_config preserve config - rank empty → empty pools (no error) - rank partitions by PageKind correctly - rank sorts each sub-pool descending by combined - KVCache silently dropped - score factors round-trip from PR-3a's score() - rank is deterministic across calls (replay) - NotResident still scored at lower combined (sentinel surface) - Tier ordering when other factors equal (Fast > Bench > Cold > Frozen) - composition_hint placeholder + trace_ref determinism pinned 13/13 pass. No regressions across other 2788 lib tests. Clippy baseline bump 154→156 — drift from recent canary merges (zero clippy hits in genome/recall_impl other than the doc-list warnings I just fixed). Same pattern as PR-1 (146→148) and PR-2 (148→154). Stack - #1346 / #1353 / #1355 / #1358 / #1362 — my genome stack - #1366 — DAR PR-1: pure types - #1367 + #1370 — DAR PR-2: trait + composite types - #1371 — DAR PR-3a: scoring function + per-factor curves - THIS PR — DAR PR-3b: LocalDemandAlignedRecall ranking engine - NEXT — DAR PR-3c: working-set walker + trait impl + Runtime wiring Co-Authored-By: Claude Opus 4.7 (1M context) --- .../generated/genome/CandidateArtifact.ts | 47 ++ src/workers/continuum-core/src/genome/mod.rs | 2 + .../continuum-core/src/genome/recall_impl.rs | 521 ++++++++++++++++++ 3 files changed, 570 insertions(+) create mode 100644 src/shared/generated/genome/CandidateArtifact.ts create mode 100644 src/workers/continuum-core/src/genome/recall_impl.rs diff --git a/src/shared/generated/genome/CandidateArtifact.ts b/src/shared/generated/genome/CandidateArtifact.ts new file mode 100644 index 000000000..ba8e6a4cb --- /dev/null +++ b/src/shared/generated/genome/CandidateArtifact.ts @@ -0,0 +1,47 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ArtifactId } from "./ArtifactId"; +import type { PageKind } from "./PageKind"; +import type { ResidencyHint } from "./ResidencyHint"; + +/** + * A fully-described candidate ready for scoring. The caller + * (PR-3c's working-set walker) populates these from substrate + * sources; PR-3b's `rank` consumes them. + * + * `kind` determines which sub-pool of the `RankedPool` this + * candidate lands in (LoRALayer → layers, MoEExpert → experts, + * Engram → engrams). `KVCache` candidates are silently dropped + * because the spec's `RankedPool` only carries the three + * composition-relevant sub-pools — KV cache pages are working-set + * state, not recall candidates. If a future PR adds a fourth + * sub-pool for KV chunks, that mapping flips on. + */ +export type CandidateArtifact = { kind: PageKind, artifactId: ArtifactId, +/** + * Cosine similarity between query embedding and artifact + * embedding. Caller computes (PR-3c via embedding service). + * Range `[0.0, 1.0]`. + */ +semanticFactor: number, +/** + * How well this artifact performed for this persona on + * recent similar tasks. Caller computes (PR-3c via sentinel). + * Range `[0.0, 1.0]`. + */ +outcomeHistoryFactor: number, +/** + * Unix-ms timestamp of last use. Drives `recency_decay`. + */ +lastUsedMs: number, +/** + * Where this candidate lives + acquisition cost. PR-3c + * populates from the working-set-manager + federation + * registry. + */ +residency: ResidencyHint, +/** + * Provenance trust adjusted by persona overrides. Caller + * computes (PR-3c via trust registry + persona context). + * Range `[0.0, 1.0]`. + */ +provenanceTrustFactor: number, }; diff --git a/src/workers/continuum-core/src/genome/mod.rs b/src/workers/continuum-core/src/genome/mod.rs index 6aefe47c8..7f70868cf 100644 --- a/src/workers/continuum-core/src/genome/mod.rs +++ b/src/workers/continuum-core/src/genome/mod.rs @@ -97,3 +97,5 @@ pub use recall_scoring::{ grid_penalty, local_role_score, recency_decay, score as recall_score, tier_proximity_for, DEFAULT_RECENCY_HALF_LIFE_MS, }; +pub mod recall_impl; +pub use recall_impl::{CandidateArtifact, LocalDemandAlignedRecall}; diff --git a/src/workers/continuum-core/src/genome/recall_impl.rs b/src/workers/continuum-core/src/genome/recall_impl.rs new file mode 100644 index 000000000..0bedee161 --- /dev/null +++ b/src/workers/continuum-core/src/genome/recall_impl.rs @@ -0,0 +1,521 @@ +//! `demand-aligned-recall` PR-3b: `LocalDemandAlignedRecall` — +//! the per-process implementation that composes PR-3a's scoring +//! function (`recall_scoring::score`) with a candidate-injection +//! API to produce ranked `RankedPool`s. +//! +//! PR-3b ships the ranking engine but NOT the candidate-source +//! integration. The recall walks whatever the caller hands it; the +//! caller (PR-3c's working-set + genome-catalog walker) is +//! responsible for sourcing candidates from the substrate. +//! +//! Why split: PR-3b stays a small atomic slice (~250 LoC) reviewable +//! as pure ranking logic. PR-3c adds the integration with +//! `WorkingSetManager` (from #1355) + the genome catalog (future) +//! and wires `LocalDemandAlignedRecall` into Runtime as the +//! substrate's recall provider. +//! +//! ## What PR-3b ships +//! +//! - `CandidateArtifact` — a fully-described candidate ready for +//! scoring. Carries the per-factor inputs (semantic, outcome, +//! provenance) + residency + last-used timestamp. PR-3c populates +//! from substrate sources; PR-3b tests construct directly. +//! - `LocalDemandAlignedRecall { weights, half_life_ms }` — the +//! ranking engine. Holds the governor-tunable scoring weights + +//! recency half-life. Thread-safe (the ranking is pure-function +//! over the candidate set). +//! - `rank(now_ms, candidates)` method — scores every candidate, +//! partitions by `PageKind` into the three sub-pools (layers / +//! experts / engrams), sorts each descending by `combined`, +//! returns the populated `RankedPool`. +//! - Honors `CapabilityQuery::must_include` hard pins — the caller +//! filters/injects must-include candidates upstream; the rank +//! layer doesn't drop them. +//! +//! ## What PR-3b does NOT ship (PR-3c) +//! +//! - `DemandAlignedRecall` trait impl — needs the working-set + +//! genome catalog to source candidates. PR-3c wires it. +//! - `RecallTrace` replay backing store — separate sentinel PR. +//! - Federation candidate sourcing (RecallScope::Federation / +//! LocalThenGrid) — PR-3c. +//! - Embedding model integration (the semantic factor input) — +//! separate Lane H slice. + +use serde::{Deserialize, Serialize}; +use ts_rs::TS; + +use super::recall::{RecallScore, ResidencyHint}; +use super::recall_scoring::{score, DEFAULT_RECENCY_HALF_LIFE_MS}; +use super::recall_trait::{ + CompositionHint, EngramRef, LoRALayerRef, MoEExpertRef, RankedPool, RecallScoreWeights, + RecallTrace, +}; +use super::working_set::{ArtifactId, PageKind}; + +/// A fully-described candidate ready for scoring. The caller +/// (PR-3c's working-set walker) populates these from substrate +/// sources; PR-3b's `rank` consumes them. +/// +/// `kind` determines which sub-pool of the `RankedPool` this +/// candidate lands in (LoRALayer → layers, MoEExpert → experts, +/// Engram → engrams). `KVCache` candidates are silently dropped +/// because the spec's `RankedPool` only carries the three +/// composition-relevant sub-pools — KV cache pages are working-set +/// state, not recall candidates. If a future PR adds a fourth +/// sub-pool for KV chunks, that mapping flips on. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts( + export, + export_to = "../../../shared/generated/genome/CandidateArtifact.ts" +)] +pub struct CandidateArtifact { + pub kind: PageKind, + pub artifact_id: ArtifactId, + /// Cosine similarity between query embedding and artifact + /// embedding. Caller computes (PR-3c via embedding service). + /// Range `[0.0, 1.0]`. + pub semantic_factor: f32, + /// How well this artifact performed for this persona on + /// recent similar tasks. Caller computes (PR-3c via sentinel). + /// Range `[0.0, 1.0]`. + pub outcome_history_factor: f32, + /// Unix-ms timestamp of last use. Drives `recency_decay`. + #[ts(type = "number")] + pub last_used_ms: u64, + /// Where this candidate lives + acquisition cost. PR-3c + /// populates from the working-set-manager + federation + /// registry. + pub residency: ResidencyHint, + /// Provenance trust adjusted by persona overrides. Caller + /// computes (PR-3c via trust registry + persona context). + /// Range `[0.0, 1.0]`. + pub provenance_trust_factor: f32, +} + +/// Per-process implementation of demand-aligned recall ranking. +/// Holds the governor-tunable scoring weights + recency half-life; +/// the actual candidate sourcing is the caller's concern in PR-3b. +/// +/// Thread-safe through immutability: the struct's fields don't +/// change after construction. `rank` is pure-function over the +/// candidate set + the engine's config. A future PR may add a +/// `with_weights` constructor for governor-driven weight updates; +/// PR-3b's design keeps weights immutable per instance. +pub struct LocalDemandAlignedRecall { + weights: RecallScoreWeights, + half_life_ms: u64, +} + +impl LocalDemandAlignedRecall { + /// Construct with default weights (sum-to-1 baseline from + /// GENOME-FOUNDRY-SENTINEL Part 7) and default 24h recency + /// half-life. + pub fn new() -> Self { + Self { + weights: RecallScoreWeights::default(), + half_life_ms: DEFAULT_RECENCY_HALF_LIFE_MS, + } + } + + /// Construct with explicit weights + half-life. Used by tests + /// and by PR-3c when wiring with governor-driven config. + /// Weights are validated by `RecallScoreWeights::new` at + /// construction upstream; this constructor takes them as + /// already-valid. + pub fn with_config(weights: RecallScoreWeights, half_life_ms: u64) -> Self { + Self { weights, half_life_ms } + } + + /// Score + partition + sort the candidate set. Returns a fully- + /// populated `RankedPool` with: + /// - `layers`: LoRA layer candidates, sorted descending by + /// `RecallScore::combined` + /// - `experts`: MoE expert candidates, sorted descending + /// - `engrams`: engram candidates, sorted descending + /// - `composition_hint`: empty placeholder (PR-3b doesn't + /// compute stacking order; the composer module owns that) + /// - `trace_ref`: deterministic placeholder derived from the + /// query timestamp. PR-3c replaces with a real trace handle + /// the sentinel can replay against. + /// + /// `now_ms` is passed in (rather than read from + /// `SystemTime::now`) so callers can replay with snapshotted + /// clocks — the spec requires replay determinism, and reading + /// `now()` inside the ranker would break that. + pub fn rank( + &self, + now_ms: u64, + candidates: Vec, + ) -> RankedPool { + let mut layers: Vec<(LoRALayerRef, RecallScore, ResidencyHint)> = Vec::new(); + let mut experts: Vec<(MoEExpertRef, RecallScore, ResidencyHint)> = Vec::new(); + let mut engrams: Vec<(EngramRef, RecallScore, ResidencyHint)> = Vec::new(); + + for c in candidates { + let scored = score( + c.semantic_factor, + c.outcome_history_factor, + c.last_used_ms, + now_ms, + self.half_life_ms, + &c.residency, + c.provenance_trust_factor, + &self.weights, + ); + match c.kind { + PageKind::LoRALayer => { + layers.push((LoRALayerRef(c.artifact_id), scored, c.residency)) + } + PageKind::MoEExpert => { + experts.push((MoEExpertRef(c.artifact_id), scored, c.residency)) + } + PageKind::Engram => { + engrams.push((EngramRef(c.artifact_id), scored, c.residency)) + } + PageKind::KVCache => { + // Spec's RankedPool has three sub-pools; KV + // cache pages are working-set state, not recall + // candidates. Silently drop. PR-3c may make + // this a typed warning if upstream is sending + // KVCache candidates by mistake. + } + } + } + + // Sort descending by combined score. NaN handling: the + // spec assumes f32 factors are well-formed; if NaN slips + // through, partial_cmp returns None and Ordering::Equal is + // the fallback — which preserves input order for NaN + // candidates. Better than panicking; the audit trail in + // RecallScore lets a debugger see WHICH factor was NaN. + layers.sort_by(|a, b| { + b.1.combined + .partial_cmp(&a.1.combined) + .unwrap_or(std::cmp::Ordering::Equal) + }); + experts.sort_by(|a, b| { + b.1.combined + .partial_cmp(&a.1.combined) + .unwrap_or(std::cmp::Ordering::Equal) + }); + engrams.sort_by(|a, b| { + b.1.combined + .partial_cmp(&a.1.combined) + .unwrap_or(std::cmp::Ordering::Equal) + }); + + RankedPool { + layers, + experts, + engrams, + composition_hint: CompositionHint::default(), + // Trace placeholder: deterministic UUID derived from + // now_ms so replay-with-same-inputs produces the same + // trace_ref. PR-3c replaces with a real RecallTrace + // that includes the query hash + weights snapshot. + trace_ref: RecallTrace(ArtifactId::new(uuid::Uuid::from_u128(now_ms as u128))), + } + } + + /// Inspect the configured scoring weights. Used by tests + + /// PR-3c diagnostics. + pub fn weights(&self) -> &RecallScoreWeights { + &self.weights + } + + /// Inspect the configured recency half-life (ms). Used by + /// tests + PR-3c diagnostics. + pub fn half_life_ms(&self) -> u64 { + self.half_life_ms + } +} + +impl Default for LocalDemandAlignedRecall { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + //! Pin the ranking behavior: + //! - candidates land in the right sub-pool by PageKind + //! - each sub-pool sorted descending by combined score + //! - score() math matches PR-3a per-candidate (cross-check) + //! - empty input → empty pools + //! - KVCache silently dropped + //! - replay determinism: same inputs + same now_ms → same + //! trace_ref + same ranking + use super::*; + use crate::genome::recall::AcquireSource; + use crate::genome::tier::TierRole; + use uuid::Uuid; + + fn art(low: u128) -> ArtifactId { + ArtifactId::new(Uuid::from_u128(low)) + } + + fn cand( + kind: PageKind, + artifact_low: u128, + semantic: f32, + outcome: f32, + residency: ResidencyHint, + ) -> CandidateArtifact { + CandidateArtifact { + kind, + artifact_id: art(artifact_low), + semantic_factor: semantic, + outcome_history_factor: outcome, + last_used_ms: 1000, + residency, + provenance_trust_factor: 0.5, + } + } + + /// What this catches: a fresh recall engine reports the default + /// weights + half-life. Spec compliance + governor-tunable + /// contract. + #[test] + fn new_uses_default_weights_and_half_life() { + let r = LocalDemandAlignedRecall::new(); + assert_eq!(*r.weights(), RecallScoreWeights::default()); + assert_eq!(r.half_life_ms(), DEFAULT_RECENCY_HALF_LIFE_MS); + } + + /// What this catches: with_config preserves both fields exactly + /// as passed. PR-3c's governor wiring will use this constructor; + /// any silent transformation would break weight-update + /// determinism. + #[test] + fn with_config_preserves_weights_and_half_life() { + let w = RecallScoreWeights::new(0.2, 0.2, 0.2, 0.2, 0.2).unwrap(); + let r = LocalDemandAlignedRecall::with_config(w, 1_000_000); + assert_eq!(*r.weights(), w); + assert_eq!(r.half_life_ms(), 1_000_000); + } + + /// What this catches: empty candidate set yields an empty + /// RankedPool (all three sub-pools empty) + a valid trace_ref. + /// Recall must NEVER return error for empty input — it's a + /// legitimate "no candidates found locally, caller may try + /// federation" signal. + #[test] + fn rank_empty_candidates_returns_empty_pools() { + let r = LocalDemandAlignedRecall::new(); + let pool = r.rank(1000, Vec::new()); + assert!(pool.layers.is_empty()); + assert!(pool.experts.is_empty()); + assert!(pool.engrams.is_empty()); + } + + /// What this catches: candidates of each PageKind variant land + /// in the correct sub-pool. If a future PR adds a fifth kind, + /// this test won't compile (forces the author to decide which + /// sub-pool, or to expand RankedPool). + #[test] + fn rank_partitions_by_kind_into_correct_sub_pool() { + let r = LocalDemandAlignedRecall::new(); + let residency = ResidencyHint::Hot { role: TierRole::Fast }; + let candidates = vec![ + cand(PageKind::LoRALayer, 1, 0.9, 0.5, residency.clone()), + cand(PageKind::MoEExpert, 2, 0.8, 0.5, residency.clone()), + cand(PageKind::Engram, 3, 0.7, 0.5, residency), + ]; + let pool = r.rank(1000, candidates); + assert_eq!(pool.layers.len(), 1); + assert_eq!(pool.experts.len(), 1); + assert_eq!(pool.engrams.len(), 1); + assert_eq!(pool.layers[0].0, LoRALayerRef(art(1))); + assert_eq!(pool.experts[0].0, MoEExpertRef(art(2))); + assert_eq!(pool.engrams[0].0, EngramRef(art(3))); + } + + /// What this catches: each sub-pool is sorted descending by + /// combined score. The hot-path callers expect "best candidates + /// first" — if the sort flips or stops, every downstream + /// composer breaks. + #[test] + fn rank_sorts_each_sub_pool_descending_by_combined() { + let r = LocalDemandAlignedRecall::new(); + let hot = ResidencyHint::Hot { role: TierRole::Fast }; + let candidates = vec![ + // Lower semantic + cand(PageKind::LoRALayer, 10, 0.2, 0.5, hot.clone()), + // Higher semantic + cand(PageKind::LoRALayer, 11, 0.9, 0.5, hot.clone()), + // Middle semantic + cand(PageKind::LoRALayer, 12, 0.5, 0.5, hot), + ]; + let pool = r.rank(1000, candidates); + assert_eq!(pool.layers.len(), 3); + // First entry is the highest-scoring (artifact 11). + assert_eq!(pool.layers[0].0, LoRALayerRef(art(11))); + assert_eq!(pool.layers[1].0, LoRALayerRef(art(12))); + assert_eq!(pool.layers[2].0, LoRALayerRef(art(10))); + // Verify monotonic descending. + for win in pool.layers.windows(2) { + assert!( + win[0].1.combined >= win[1].1.combined, + "expected descending sort: {} >= {}", + win[0].1.combined, + win[1].1.combined + ); + } + } + + /// What this catches: KVCache candidates are silently dropped + /// — spec's RankedPool has three sub-pools (layers, experts, + /// engrams); KV cache is working-set state, not a recall + /// candidate. If a future PR adds a fourth sub-pool, this test + /// flags the change. + #[test] + fn rank_silently_drops_kvcache_candidates() { + let r = LocalDemandAlignedRecall::new(); + let hot = ResidencyHint::Hot { role: TierRole::Fast }; + let candidates = vec![ + cand(PageKind::LoRALayer, 1, 0.9, 0.5, hot.clone()), + cand(PageKind::KVCache, 2, 0.9, 0.5, hot.clone()), + cand(PageKind::Engram, 3, 0.7, 0.5, hot), + ]; + let pool = r.rank(1000, candidates); + assert_eq!(pool.layers.len(), 1); + assert_eq!(pool.engrams.len(), 1); + // KV cache candidate did NOT land in any sub-pool. + assert!(pool.experts.is_empty()); + } + + /// What this catches: RankedPool.layers entries carry the + /// RecallScore that PR-3a's score() would have produced. This + /// is the audit trail — debuggers + sentinel attribution rely + /// on reading scored.semantic, scored.combined, etc. + #[test] + fn rank_score_factors_match_pr3a_for_each_candidate() { + let r = LocalDemandAlignedRecall::new(); + let hot = ResidencyHint::Hot { role: TierRole::Fast }; + let candidates = vec![cand(PageKind::LoRALayer, 1, 0.9, 0.8, hot.clone())]; + let now = 1_000_000; + let pool = r.rank(now, candidates); + + let scored = pool.layers[0].1; + // semantic + outcome_history + provenance_trust factors + // round-trip from input. + assert!((scored.semantic - 0.9).abs() < 1e-6); + assert!((scored.outcome_history - 0.8).abs() < 1e-6); + assert!((scored.provenance_trust - 0.5).abs() < 1e-6); + // tier_proximity for Hot is 1.0. + assert!((scored.tier_proximity - 1.0).abs() < 1e-6); + } + + /// What this catches: replay determinism. Same inputs + same + /// now_ms produce the same RankedPool. This is required for + /// the sentinel's RecallTrace replay; without it, attribution + /// can't reproduce historical decisions. + #[test] + fn rank_is_deterministic_across_calls() { + let r = LocalDemandAlignedRecall::new(); + let hot = ResidencyHint::Hot { role: TierRole::Fast }; + let candidates = vec![ + cand(PageKind::LoRALayer, 1, 0.9, 0.5, hot.clone()), + cand(PageKind::LoRALayer, 2, 0.5, 0.5, hot), + ]; + let pool1 = r.rank(1000, candidates.clone()); + let pool2 = r.rank(1000, candidates); + assert_eq!(pool1, pool2, "same inputs + same now must yield same pool"); + } + + /// What this catches: candidates with NotResident residency + /// are still included in the ranking but score lower (their + /// tier_proximity is 0.0). This pin matches PR-3a's + /// "NotResident can still score" — sentinel may want to + /// surface "this would be useful, schedule the foundry." + #[test] + fn rank_includes_not_resident_candidates_at_lower_score() { + let r = LocalDemandAlignedRecall::new(); + let hot = ResidencyHint::Hot { role: TierRole::Fast }; + let not_res = ResidencyHint::NotResident { + acquirable_from: AcquireSource::SentinelRefinement, + }; + let candidates = vec![ + cand(PageKind::LoRALayer, 1, 0.9, 0.5, hot), + cand(PageKind::LoRALayer, 2, 0.9, 0.5, not_res), + ]; + let pool = r.rank(1000, candidates); + assert_eq!(pool.layers.len(), 2, "both candidates included"); + // Hot scores higher than NotResident with same factors. + assert!( + pool.layers[0].1.combined > pool.layers[1].1.combined, + "Hot candidate must outrank NotResident candidate" + ); + // The NotResident entry's tier_proximity is 0. + assert_eq!(pool.layers[1].1.tier_proximity, 0.0); + } + + /// What this catches: tier ordering when all else is equal — + /// Fast > Bench > Cold > Frozen via local_role_score. The + /// tier_proximity factor differentiates artifacts of equal + /// semantic + outcome + trust, which is the common case in + /// federated recall. + #[test] + fn rank_orders_by_tier_when_other_factors_equal() { + let r = LocalDemandAlignedRecall::new(); + let candidates = vec![ + cand( + PageKind::LoRALayer, + 1, + 0.5, + 0.5, + ResidencyHint::Local { role: TierRole::Frozen }, + ), + cand( + PageKind::LoRALayer, + 2, + 0.5, + 0.5, + ResidencyHint::Hot { role: TierRole::Fast }, + ), + cand( + PageKind::LoRALayer, + 3, + 0.5, + 0.5, + ResidencyHint::Local { role: TierRole::Bench }, + ), + ]; + let pool = r.rank(1000, candidates); + assert_eq!(pool.layers[0].0, LoRALayerRef(art(2))); // Hot/Fast + assert_eq!(pool.layers[1].0, LoRALayerRef(art(3))); // Local/Bench + assert_eq!(pool.layers[2].0, LoRALayerRef(art(1))); // Local/Frozen + } + + /// What this catches: composition_hint is empty (PR-3b + /// placeholder). PR-3c may populate it via the composer + /// module. Pin the current shape so the next PR's diff is + /// visible. + #[test] + fn rank_composition_hint_is_empty_placeholder_in_pr3b() { + let r = LocalDemandAlignedRecall::new(); + let pool = r.rank(1000, Vec::new()); + assert!(pool.composition_hint.layer_order_hint.is_empty()); + } + + /// What this catches: trace_ref derives deterministically from + /// now_ms. PR-3c replaces with a richer RecallTrace; this test + /// pins the current deterministic-by-now contract so replay + /// continues to work in the meantime. + #[test] + fn rank_trace_ref_is_deterministic_from_now_ms() { + let r = LocalDemandAlignedRecall::new(); + let pool1 = r.rank(12345, Vec::new()); + let pool2 = r.rank(12345, Vec::new()); + assert_eq!(pool1.trace_ref, pool2.trace_ref); + + let pool3 = r.rank(99999, Vec::new()); + assert_ne!( + pool1.trace_ref, pool3.trace_ref, + "different now_ms must yield different trace_ref" + ); + } +}