use anyhow::Result;
use sha2::{Digest, Sha256};
use crate::context::{ContextLimits, SESSIONSTART_RELEVANCE_POLICY_VERSION};
use super::domain::{
ChannelKind, ContextFilters, ContextIntent, ContextPlan, ContextRequest, PlannedChannel,
CONTEXT_BUNDLE_SCHEMA_VERSION,
};
use super::policy::{section_budgets, validate_request, CONTEXT_BUNDLE_POLICY_VERSION};
pub fn plan(request: &ContextRequest) -> Result<ContextPlan> {
validate_request(request)?;
let limits = ContextLimits::default();
let relevance_query = normalized_relevance_query(&request.task);
let mut plan = ContextPlan {
schema_version: CONTEXT_BUNDLE_SCHEMA_VERSION,
policy_version: CONTEXT_BUNDLE_POLICY_VERSION.to_string(),
relevance_policy_version: SESSIONSTART_RELEVANCE_POLICY_VERSION.to_string(),
intent: ContextIntent::SessionStart,
relevance_query,
relevance_k: limits.sessionstart_relevance_k as u32,
channels: planned_channels(&limits),
filters: ContextFilters {
project: request.project.key.clone(),
branch: request.branch.clone(),
include_superseded: request.include_superseded,
as_of_epoch: request.as_of_epoch,
},
section_budgets: section_budgets(request.token_budget),
plan_hash: String::new(),
};
plan.plan_hash = plan_content_hash(&plan)?;
Ok(plan)
}
fn normalized_relevance_query(task: &str) -> Option<String> {
let trimmed = task.trim();
if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
}
}
fn planned_channels(limits: &ContextLimits) -> Vec<PlannedChannel> {
vec![
PlannedChannel {
channel: ChannelKind::Preferences,
item_limit: (limits.preference_project_limit + limits.preference_global_limit) as u32,
relevance_governed: false,
},
PlannedChannel {
channel: ChannelKind::Lessons,
item_limit: limits.lesson_limit as u32,
relevance_governed: true,
},
PlannedChannel {
channel: ChannelKind::Core,
item_limit: limits.core_item_limit as u32,
relevance_governed: false,
},
PlannedChannel {
channel: ChannelKind::Workstreams,
item_limit: 5,
relevance_governed: false,
},
PlannedChannel {
channel: ChannelKind::MemoryIndex,
item_limit: limits.memory_index_limit as u32,
relevance_governed: true,
},
PlannedChannel {
channel: ChannelKind::Sessions,
item_limit: limits.session_limit as u32,
relevance_governed: true,
},
]
}
pub(super) fn plan_content_hash(plan: &ContextPlan) -> Result<String> {
let mut hashable = plan.clone();
hashable.plan_hash = String::new();
let canonical = serde_json::to_string(&hashable)?;
let mut hasher = Sha256::new();
hasher.update(canonical.as_bytes());
Ok(format!("{:x}", hasher.finalize()))
}