1use std::collections::HashMap;
9
10use crate::context::{
11 build_sessionstart_relevance_plan, RelevanceCandidate, RelevanceSection,
12 SessionStartRelevancePlan,
13};
14
15use super::audit::AuditBuilder;
16use crate::retrieval_router::{AbstentionMode, RetrievalPlan};
17
18use super::domain::{
19 ChannelKind, ContextBundle, ContextItem, DegradedMode, ItemValidity, SourceKind, TrustClass,
20 CONTEXT_BUNDLE_SCHEMA_VERSION,
21};
22use super::policy::{
23 estimate_item_tokens, validate_plan, REASON_BELOW_TRUST_FLOOR, REASON_BRANCH_SCOPE_MISMATCH,
24 REASON_CANONICAL_LOAD_FAILED, REASON_CANONICAL_ONLY_DEGRADED, REASON_CHANNEL_ITEM_LIMIT,
25 REASON_CHANNEL_TOKEN_BUDGET, REASON_PLAN_BLOCKED, REASON_POISONING_GATE,
26 REASON_PROJECT_SCOPE_MISMATCH, REASON_QUARANTINED_TRUST, REASON_SELECTED_CHANNEL,
27 REASON_SELECTED_RELEVANCE, REASON_SUPERSEDED_EXCLUDED, REASON_TOTAL_TOKEN_BUDGET,
28};
29
30#[derive(Debug, Clone)]
35pub struct ExecutorInputs {
36 pub candidates: Vec<ContextItem>,
37 pub poisoning_drops: Vec<ContextItem>,
41 pub preselection_drops: Vec<PreselectionDrop>,
44 pub enrichment_available: bool,
45}
46
47#[derive(Debug, Clone)]
48pub struct PreselectionDrop {
49 pub item: ContextItem,
50 pub reason: String,
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub(crate) enum BudgetEnforcement {
59 Strict,
60 DeferToRenderer,
61}
62
63pub(crate) struct ExecutionTrace {
64 pub bundle: ContextBundle,
65 pub relevance_plan: SessionStartRelevancePlan,
66}
67
68pub fn execute(plan: &RetrievalPlan, inputs: &ExecutorInputs) -> ContextBundle {
74 execute_with_trace(plan, inputs, BudgetEnforcement::Strict).bundle
75}
76
77pub(crate) fn execute_with_trace(
78 plan: &RetrievalPlan,
79 inputs: &ExecutorInputs,
80 budget_enforcement: BudgetEnforcement,
81) -> ExecutionTrace {
82 if let Err(error) = validate_plan(plan) {
83 return ExecutionTrace {
84 bundle: blocked_bundle(plan, inputs, &error.to_string()),
85 relevance_plan: SessionStartRelevancePlan::disabled(&[]),
86 };
87 }
88 let degraded_mode = if inputs.enrichment_available {
89 DegradedMode::Full
90 } else {
91 DegradedMode::CanonicalOnly
92 };
93
94 let mut audit = AuditBuilder::default();
95 for item in &inputs.poisoning_drops {
96 audit.dropped(item, REASON_POISONING_GATE);
97 }
98 for dropped in &inputs.preselection_drops {
99 audit.dropped(&dropped.item, &dropped.reason);
100 }
101 let mut in_scope: Vec<&ContextItem> = Vec::new();
102 for item in &inputs.candidates {
103 match scope_drop_reason(plan, degraded_mode, item) {
104 Some(reason) => audit.dropped(item, reason),
105 None => in_scope.push(item),
106 }
107 }
108
109 let (relevance, relevance_plan) = relevance_decisions(plan, &in_scope, &mut audit);
110 let mut survivors: Vec<&ContextItem> = Vec::new();
111 for item in in_scope {
112 let governed = channel_relevance_governed(plan, item.channel);
113 if governed {
114 match relevance.get(item.stable_key.as_str()) {
115 Some(&(true, _)) | None => survivors.push(item),
116 Some(&(false, drop_reason)) => audit.dropped(item, drop_reason),
117 }
118 } else {
119 survivors.push(item);
120 }
121 }
122
123 let mut bundle = empty_bundle(plan, degraded_mode);
124 match budget_enforcement {
125 BudgetEnforcement::Strict => {
126 order_relevance_governed_survivors(plan, &relevance_plan, &mut survivors);
127 apply_budgets(plan, degraded_mode, &survivors, &mut bundle, &mut audit)
128 }
129 BudgetEnforcement::DeferToRenderer => {
130 select_for_renderer(plan, &survivors, &mut bundle, &mut audit)
131 }
132 }
133 if plan.abstention_policy.mode == AbstentionMode::OnLowEvidence
134 && audit.selected_count() < plan.abstention_policy.min_selected_items
135 {
136 clear_bundle_sections(&mut bundle);
137 audit.abstain();
138 }
139 bundle.audit = audit.finalize(plan, degraded_mode);
140 ExecutionTrace {
141 bundle,
142 relevance_plan,
143 }
144}
145
146fn order_relevance_governed_survivors(
150 plan: &RetrievalPlan,
151 relevance_plan: &SessionStartRelevancePlan,
152 survivors: &mut [&ContextItem],
153) {
154 let ranks = relevance_plan
155 .selected_keys()
156 .iter()
157 .enumerate()
158 .map(|(rank, key)| (key.as_str(), rank))
159 .collect::<HashMap<_, _>>();
160 survivors.sort_by_key(|item| {
161 if channel_relevance_governed(plan, item.channel) {
162 ranks
163 .get(item.stable_key.as_str())
164 .copied()
165 .unwrap_or(usize::MAX)
166 } else {
167 usize::MAX
168 }
169 });
170}
171
172fn scope_drop_reason(
173 plan: &RetrievalPlan,
174 degraded_mode: DegradedMode,
175 item: &ContextItem,
176) -> Option<&'static str> {
177 if item.trust == TrustClass::Quarantined {
178 return Some(REASON_QUARANTINED_TRUST);
179 }
180 if trust_rank(item.trust) < trust_rank(plan.trust_policy.minimum_trust) {
181 return Some(REASON_BELOW_TRUST_FLOOR);
182 }
183 if let Some(project) = &item.project {
184 if project != &plan.filters.project {
185 return Some(REASON_PROJECT_SCOPE_MISMATCH);
186 }
187 }
188 if let (Some(item_branch), Some(plan_branch)) = (&item.branch, &plan.filters.branch) {
189 if item_branch != plan_branch {
190 return Some(REASON_BRANCH_SCOPE_MISMATCH);
191 }
192 }
193 if item.validity == ItemValidity::Superseded && !plan.filters.include_superseded {
194 return Some(REASON_SUPERSEDED_EXCLUDED);
195 }
196 if degraded_mode == DegradedMode::CanonicalOnly && item.source_kind != SourceKind::Canonical {
197 return Some(REASON_CANONICAL_ONLY_DEGRADED);
198 }
199 None
200}
201
202fn trust_rank(trust: TrustClass) -> u8 {
203 match trust {
204 TrustClass::Quarantined => 0,
205 TrustClass::Standard => 1,
206 TrustClass::Trusted => 2,
207 }
208}
209
210fn clear_bundle_sections(bundle: &mut ContextBundle) {
211 bundle.preferences.clear();
212 bundle.failure_lessons.clear();
213 bundle.current_truth.clear();
214 bundle.workstreams.clear();
215 bundle.memory_index.clear();
216 bundle.recent_sessions.clear();
217}
218
219fn channel_relevance_governed(plan: &RetrievalPlan, channel: ChannelKind) -> bool {
220 plan.output_sections
221 .iter()
222 .find(|planned| planned.channel == channel)
223 .is_some_and(|planned| planned.relevance_governed)
224}
225
226fn relevance_section(channel: ChannelKind) -> Option<RelevanceSection> {
227 match channel {
228 ChannelKind::Lessons => Some(RelevanceSection::Lessons),
229 ChannelKind::MemoryIndex => Some(RelevanceSection::MemoryIndex),
230 ChannelKind::Sessions => Some(RelevanceSection::Sessions),
231 ChannelKind::Preferences | ChannelKind::Core | ChannelKind::Workstreams => None,
232 }
233}
234
235fn relevance_decisions<'a>(
239 plan: &RetrievalPlan,
240 in_scope: &[&'a ContextItem],
241 audit: &mut AuditBuilder,
242) -> (
243 HashMap<&'a str, (bool, &'static str)>,
244 SessionStartRelevancePlan,
245) {
246 let candidates: Vec<RelevanceCandidate> = in_scope
247 .iter()
248 .filter(|item| channel_relevance_governed(plan, item.channel))
249 .filter_map(|item| {
250 relevance_section(item.channel).map(|section| RelevanceCandidate {
251 stable_key: item.stable_key.clone(),
252 section,
253 text: format!("{} {}", item.title, item.text),
254 })
255 })
256 .collect();
257 let relevance_plan = build_sessionstart_relevance_plan(
258 plan.relevance_query.as_deref(),
259 plan.relevance_k as usize,
260 &candidates,
261 );
262 let mut decisions = HashMap::new();
263 for item in in_scope {
264 let Some(decision) = relevance_plan.decision(&item.stable_key) else {
265 continue;
266 };
267 audit.record_score(&item.stable_key, decision.score);
268 decisions.insert(
269 item.stable_key.as_str(),
270 (decision.selected, decision.drop_reason.unwrap_or("dropped")),
271 );
272 }
273 (decisions, relevance_plan)
274}
275
276fn select_for_renderer(
282 plan: &RetrievalPlan,
283 survivors: &[&ContextItem],
284 bundle: &mut ContextBundle,
285 audit: &mut AuditBuilder,
286) {
287 for item in survivors {
288 let governed = channel_relevance_governed(plan, item.channel);
289 let reason = if governed {
290 REASON_SELECTED_RELEVANCE
291 } else {
292 REASON_SELECTED_CHANNEL
293 };
294 audit.selected(item, reason);
295 bundle.section_mut(item.channel).push((*item).clone());
296 }
297}
298
299fn apply_budgets(
302 plan: &RetrievalPlan,
303 _degraded_mode: DegradedMode,
304 survivors: &[&ContextItem],
305 bundle: &mut ContextBundle,
306 audit: &mut AuditBuilder,
307) {
308 let mut total_tokens: u32 = 0;
309 let total_budget = plan.section_budgets.total_tokens;
310 for channel in ChannelKind::ORDERED {
311 let item_limit = plan
312 .output_sections
313 .iter()
314 .find(|planned| planned.channel == channel)
315 .map(|planned| planned.item_limit)
316 .unwrap_or(0);
317 let channel_budget = plan.section_budgets.for_channel(channel);
318 let governed = channel_relevance_governed(plan, channel);
319 let mut channel_tokens: u32 = 0;
320 let mut channel_count: u32 = 0;
321 for item in survivors.iter().filter(|item| item.channel == channel) {
322 let tokens = estimate_item_tokens(item);
323 if channel_count >= item_limit {
324 audit.dropped(item, REASON_CHANNEL_ITEM_LIMIT);
325 continue;
326 }
327 if channel_tokens + tokens > channel_budget {
328 audit.dropped(item, REASON_CHANNEL_TOKEN_BUDGET);
329 continue;
330 }
331 if total_tokens + tokens > total_budget {
332 audit.dropped(item, REASON_TOTAL_TOKEN_BUDGET);
333 audit.set_truncation_reason(REASON_TOTAL_TOKEN_BUDGET);
334 continue;
335 }
336 channel_count += 1;
337 channel_tokens += tokens;
338 total_tokens += tokens;
339 let reason = if governed {
340 REASON_SELECTED_RELEVANCE
341 } else {
342 REASON_SELECTED_CHANNEL
343 };
344 audit.selected(item, reason);
345 bundle.section_mut(channel).push((*item).clone());
346 }
347 }
348}
349
350fn empty_bundle(plan: &RetrievalPlan, degraded_mode: DegradedMode) -> ContextBundle {
351 ContextBundle {
352 schema_version: CONTEXT_BUNDLE_SCHEMA_VERSION,
353 plan_hash: plan.plan_hash.clone(),
354 degraded_mode,
355 preferences: Vec::new(),
356 failure_lessons: Vec::new(),
357 current_truth: Vec::new(),
358 workstreams: Vec::new(),
359 memory_index: Vec::new(),
360 recent_sessions: Vec::new(),
361 audit: super::domain::ContextAudit {
362 schema_version: CONTEXT_BUNDLE_SCHEMA_VERSION,
363 policy_version: plan.policy_version.clone(),
364 relevance_policy_version: plan.relevance_policy_version.clone(),
365 plan_hash: plan.plan_hash.clone(),
366 degraded_mode,
367 candidates_considered: 0,
368 selected_count: 0,
369 dropped_count: 0,
370 token_estimate: 0,
371 token_budget: plan.section_budgets.total_tokens,
372 truncation_reason: None,
373 entries: Vec::new(),
374 shadow_comparison: Vec::new(),
375 },
376 }
377}
378
379fn blocked_bundle(plan: &RetrievalPlan, inputs: &ExecutorInputs, error: &str) -> ContextBundle {
380 crate::log::error(
381 "context-bundle",
382 &format!("plan validation failed; emitting blocked bundle: {error}"),
383 );
384 let mut audit = AuditBuilder::default();
385 for item in &inputs.candidates {
386 audit.dropped(item, REASON_PLAN_BLOCKED);
387 }
388 for item in &inputs.poisoning_drops {
389 audit.dropped(item, REASON_POISONING_GATE);
390 }
391 for dropped in &inputs.preselection_drops {
392 audit.dropped(&dropped.item, &dropped.reason);
393 }
394 audit.set_truncation_reason(REASON_PLAN_BLOCKED);
395 let mut bundle = empty_bundle(plan, DegradedMode::Blocked);
396 bundle.audit = audit.finalize(plan, DegradedMode::Blocked);
397 bundle
398}
399
400pub fn blocked_before_load(plan: &RetrievalPlan, reason: &str) -> ContextBundle {
407 crate::log::error(
408 "context-bundle",
409 &format!("canonical load failed; emitting blocked bundle: {reason}"),
410 );
411 let mut audit = AuditBuilder::default();
412 audit.set_truncation_reason(REASON_CANONICAL_LOAD_FAILED);
413 let mut bundle = empty_bundle(plan, DegradedMode::Blocked);
414 bundle.audit = audit.finalize(plan, DegradedMode::Blocked);
415 bundle
416}