#![allow(dead_code)]
use std::collections::{BTreeMap, BTreeSet, VecDeque, btree_map::Entry};
use std::fs::{File, OpenOptions};
use std::io::{self, BufRead, BufReader, Write};
use std::path::{Path, PathBuf};
use std::sync::{Mutex, OnceLock};
use std::time::UNIX_EPOCH;
use chrono::{DateTime, Duration, NaiveDate, Utc};
use serde::{Deserialize, Serialize};
use crate::storage::RepositoryManifestMetadataSnapshot;
pub(crate) const CONTEXT_EFFICIENCY_LOG_ENV: &str = "FRIGG_CONTEXT_EFFICIENCY_LOG";
const MANIFEST_METADATA_CACHE_LIMIT: usize = 64;
const JSONL_SUMMARY_CACHE_LIMIT: usize = 32;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) struct ManifestMetadataCacheKey {
storage_identity: String,
repository_id: String,
snapshot_id: String,
}
impl ManifestMetadataCacheKey {
pub(crate) fn new(
storage_identity: impl Into<String>,
repository_id: impl Into<String>,
snapshot_id: impl Into<String>,
) -> Self {
Self {
storage_identity: storage_identity.into(),
repository_id: repository_id.into(),
snapshot_id: snapshot_id.into(),
}
}
pub(crate) fn from_storage_path(
storage_path: &Path,
repository_id: &str,
snapshot_id: &str,
) -> Self {
Self::new(
storage_path.to_string_lossy().into_owned(),
repository_id.to_owned(),
snapshot_id.to_owned(),
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ManifestMetadataSummary {
pub(crate) repository_id: String,
pub(crate) snapshot_id: String,
pub(crate) indexed_readable_files: usize,
pub(crate) indexed_readable_bytes: u64,
pub(crate) min_mtime_ns: Option<u64>,
pub(crate) max_mtime_ns: Option<u64>,
pub(crate) path_size_bytes: BTreeMap<String, u64>,
}
impl ManifestMetadataSummary {
pub(crate) fn from_snapshot(snapshot: &RepositoryManifestMetadataSnapshot) -> Self {
let mut indexed_readable_bytes = 0_u64;
let mut min_mtime_ns = None;
let mut max_mtime_ns = None;
let mut path_size_bytes = BTreeMap::new();
for entry in &snapshot.entries {
indexed_readable_bytes = indexed_readable_bytes.saturating_add(entry.size_bytes);
if let Some(mtime_ns) = entry.mtime_ns {
min_mtime_ns =
Some(min_mtime_ns.map_or(mtime_ns, |current: u64| current.min(mtime_ns)));
max_mtime_ns =
Some(max_mtime_ns.map_or(mtime_ns, |current: u64| current.max(mtime_ns)));
}
path_size_bytes.insert(entry.path.clone(), entry.size_bytes);
}
Self {
repository_id: snapshot.repository_id.clone(),
snapshot_id: snapshot.snapshot_id.clone(),
indexed_readable_files: snapshot.entries.len(),
indexed_readable_bytes,
min_mtime_ns,
max_mtime_ns,
path_size_bytes,
}
}
pub(crate) fn returned_unique_file_bytes<'a>(
&self,
paths: impl IntoIterator<Item = &'a str>,
) -> u64 {
let unique_paths = paths.into_iter().collect::<BTreeSet<_>>();
unique_paths
.into_iter()
.filter_map(|path| self.path_size_bytes.get(path))
.copied()
.fold(0_u64, u64::saturating_add)
}
pub(crate) fn returned_unique_file_bytes_if_known<'a>(
&self,
paths: impl IntoIterator<Item = &'a str>,
) -> Option<u64> {
let unique_paths = paths.into_iter().collect::<BTreeSet<_>>();
let mut total = 0_u64;
for path in unique_paths {
total = total.saturating_add(*self.path_size_bytes.get(path)?);
}
Some(total)
}
pub(crate) fn file_size_bytes_for_path(
&self,
workspace_root: Option<&Path>,
path: &str,
) -> Option<u64> {
if let Some(size_bytes) = self.path_size_bytes.get(path).copied() {
return Some(size_bytes);
}
let workspace_root = workspace_root?;
let requested_path = Path::new(path);
if requested_path.is_relative() {
let absolute_path = workspace_root.join(requested_path);
return self
.path_size_bytes
.get(&absolute_path.to_string_lossy().into_owned())
.copied();
}
let relative_path = requested_path.strip_prefix(workspace_root).ok()?;
self.path_size_bytes
.get(&relative_path.to_string_lossy().into_owned())
.copied()
}
}
#[derive(Debug, Default)]
struct ManifestMetadataCache {
insertion_order: VecDeque<ManifestMetadataCacheKey>,
entries: BTreeMap<ManifestMetadataCacheKey, ManifestMetadataSummary>,
}
impl ManifestMetadataCache {
fn get(&self, key: &ManifestMetadataCacheKey) -> Option<ManifestMetadataSummary> {
self.entries.get(key).cloned()
}
fn insert(&mut self, key: ManifestMetadataCacheKey, value: ManifestMetadataSummary) {
match self.entries.entry(key) {
Entry::Occupied(mut entry) => {
entry.insert(value);
return;
}
Entry::Vacant(entry) => {
self.insertion_order.push_back(entry.key().clone());
entry.insert(value);
}
}
while self.entries.len() > MANIFEST_METADATA_CACHE_LIMIT {
let Some(oldest_key) = self.insertion_order.pop_front() else {
break;
};
self.entries.remove(&oldest_key);
}
}
}
static MANIFEST_METADATA_CACHE: OnceLock<Mutex<ManifestMetadataCache>> = OnceLock::new();
fn manifest_metadata_cache() -> &'static Mutex<ManifestMetadataCache> {
MANIFEST_METADATA_CACHE.get_or_init(|| Mutex::new(ManifestMetadataCache::default()))
}
pub(crate) fn cached_manifest_metadata_summary(
key: &ManifestMetadataCacheKey,
) -> Option<ManifestMetadataSummary> {
manifest_metadata_cache()
.lock()
.expect("context-efficiency manifest cache mutex poisoned")
.get(key)
}
pub(crate) fn store_manifest_metadata_summary(
key: ManifestMetadataCacheKey,
value: ManifestMetadataSummary,
) {
manifest_metadata_cache()
.lock()
.expect("context-efficiency manifest cache mutex poisoned")
.insert(key, value);
}
pub(crate) fn is_truthy_env_value(value: &str) -> bool {
let normalized = value.trim().to_ascii_lowercase();
!matches!(normalized.as_str(), "" | "0" | "false" | "no" | "off")
}
pub(crate) fn context_efficiency_log_enabled() -> bool {
std::env::var(CONTEXT_EFFICIENCY_LOG_ENV)
.map(|value| is_truthy_env_value(&value))
.unwrap_or(false)
}
pub(crate) fn need_context_efficiency(include_context_efficiency: Option<bool>) -> bool {
need_context_efficiency_with_log_state(
include_context_efficiency,
context_efficiency_log_enabled(),
)
}
pub(crate) fn need_context_efficiency_with_log_state(
include_context_efficiency: Option<bool>,
context_efficiency_log_enabled: bool,
) -> bool {
include_context_efficiency == Some(true) || context_efficiency_log_enabled
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub(crate) struct ContextEfficiencyLogMetrics {
pub(crate) indexed_readable_files: usize,
pub(crate) indexed_readable_bytes: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) indexed_min_mtime_ns: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) indexed_max_mtime_ns: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) candidate_input_count: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) candidate_output_count: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) returned_match_count: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) returned_unique_paths: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) returned_unique_file_bytes: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) returned_source_bytes_estimate: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) matched_file_context_saved_bytes_estimate: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) matched_file_context_saved_percent_estimate: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) query_duration_ms: Option<u64>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_optional_u64_from_number"
)]
pub(crate) narrowing_ratio_estimate: Option<u64>,
}
fn deserialize_optional_u64_from_number<'de, D>(deserializer: D) -> Result<Option<u64>, D::Error>
where
D: serde::Deserializer<'de>,
{
let Some(value) = Option::<serde_json::Value>::deserialize(deserializer)? else {
return Ok(None);
};
match value {
serde_json::Value::Number(number) => {
if let Some(value) = number.as_u64() {
return Ok(Some(value));
}
let value = number.as_f64().ok_or_else(|| {
serde::de::Error::custom("expected finite non-negative numeric ratio")
})?;
if !value.is_finite() || value < 0.0 {
return Err(serde::de::Error::custom(
"expected finite non-negative numeric ratio",
));
}
Ok(Some(value.round() as u64))
}
other => Err(serde::de::Error::custom(format!(
"expected numeric ratio, got {other}"
))),
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub(crate) struct ContextEfficiencyLogRow {
pub(crate) timestamp: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) session_id: Option<String>,
pub(crate) tool: String,
pub(crate) repository_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) snapshot_id: Option<String>,
#[serde(flatten)]
pub(crate) metrics: ContextEfficiencyLogMetrics,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ContextSummaryWindow {
pub date_since: DateTime<Utc>,
pub date_until: DateTime<Utc>,
}
impl ContextSummaryWindow {
pub fn resolve(
since: Option<&str>,
until: Option<&str>,
now: DateTime<Utc>,
) -> Result<Self, ContextSummaryError> {
let parsed_since = since
.map(parse_context_summary_datetime)
.transpose()
.map_err(ContextSummaryError::InvalidDate)?;
let parsed_until = until
.map(parse_context_summary_datetime)
.transpose()
.map_err(ContextSummaryError::InvalidDate)?;
let (date_since, date_until) = match (parsed_since, parsed_until) {
(Some(date_since), Some(date_until)) => (date_since, date_until),
(Some(date_since), None) => (date_since, now),
(None, Some(date_until)) => (date_until - Duration::days(30), date_until),
(None, None) => (now - Duration::days(30), now),
};
if date_since > date_until {
return Err(ContextSummaryError::InvalidDate(format!(
"date_since must be before or equal to date_until: {} > {}",
date_since.to_rfc3339(),
date_until.to_rfc3339()
)));
}
Ok(Self {
date_since,
date_until,
})
}
fn since_key(&self) -> String {
self.date_since.to_rfc3339()
}
fn until_key(&self) -> String {
self.date_until.to_rfc3339()
}
}
#[derive(Debug)]
pub enum ContextSummaryError {
InvalidDate(String),
Io(io::Error),
Json(serde_json::Error),
}
impl std::fmt::Display for ContextSummaryError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidDate(message) => formatter.write_str(message),
Self::Io(error) => error.fmt(formatter),
Self::Json(error) => error.fmt(formatter),
}
}
}
impl std::error::Error for ContextSummaryError {}
impl From<io::Error> for ContextSummaryError {
fn from(error: io::Error) -> Self {
Self::Io(error)
}
}
impl From<serde_json::Error> for ContextSummaryError {
fn from(error: serde_json::Error) -> Self {
Self::Json(error)
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
pub struct ContextLogAggregate {
pub events: usize,
pub indexed_readable_files_max: usize,
pub indexed_readable_bytes_max: u64,
pub query_duration_ms_total: u64,
pub query_duration_ms_max: u64,
pub candidate_input_count: u64,
pub candidate_output_count: u64,
pub returned_match_count: u64,
pub returned_unique_paths: u64,
pub returned_unique_file_bytes: u64,
pub returned_source_bytes_estimate: u64,
pub matched_file_context_saved_bytes_estimate: i64,
}
impl ContextLogAggregate {
fn add_row(&mut self, row: &ContextEfficiencyLogRow) {
self.events = self.events.saturating_add(1);
self.indexed_readable_files_max = self
.indexed_readable_files_max
.max(row.metrics.indexed_readable_files);
self.indexed_readable_bytes_max = self
.indexed_readable_bytes_max
.max(row.metrics.indexed_readable_bytes);
self.query_duration_ms_total = self
.query_duration_ms_total
.saturating_add(row.metrics.query_duration_ms.unwrap_or_default());
self.query_duration_ms_max = self
.query_duration_ms_max
.max(row.metrics.query_duration_ms.unwrap_or_default());
self.candidate_input_count = self
.candidate_input_count
.saturating_add(row.metrics.candidate_input_count.unwrap_or_default() as u64);
self.candidate_output_count = self
.candidate_output_count
.saturating_add(row.metrics.candidate_output_count.unwrap_or_default() as u64);
self.returned_match_count = self
.returned_match_count
.saturating_add(row.metrics.returned_match_count.unwrap_or_default() as u64);
self.returned_unique_paths = self
.returned_unique_paths
.saturating_add(row.metrics.returned_unique_paths.unwrap_or_default() as u64);
self.returned_unique_file_bytes = self
.returned_unique_file_bytes
.saturating_add(row.metrics.returned_unique_file_bytes.unwrap_or_default());
self.returned_source_bytes_estimate = self.returned_source_bytes_estimate.saturating_add(
row.metrics
.returned_source_bytes_estimate
.unwrap_or_default(),
);
self.matched_file_context_saved_bytes_estimate = self
.matched_file_context_saved_bytes_estimate
.saturating_add(
row.metrics
.matched_file_context_saved_bytes_estimate
.unwrap_or_default(),
);
}
fn merge(&mut self, other: &Self) {
self.events = self.events.saturating_add(other.events);
self.indexed_readable_files_max = self
.indexed_readable_files_max
.max(other.indexed_readable_files_max);
self.indexed_readable_bytes_max = self
.indexed_readable_bytes_max
.max(other.indexed_readable_bytes_max);
self.query_duration_ms_total = self
.query_duration_ms_total
.saturating_add(other.query_duration_ms_total);
self.query_duration_ms_max = self.query_duration_ms_max.max(other.query_duration_ms_max);
self.candidate_input_count = self
.candidate_input_count
.saturating_add(other.candidate_input_count);
self.candidate_output_count = self
.candidate_output_count
.saturating_add(other.candidate_output_count);
self.returned_match_count = self
.returned_match_count
.saturating_add(other.returned_match_count);
self.returned_unique_paths = self
.returned_unique_paths
.saturating_add(other.returned_unique_paths);
self.returned_unique_file_bytes = self
.returned_unique_file_bytes
.saturating_add(other.returned_unique_file_bytes);
self.returned_source_bytes_estimate = self
.returned_source_bytes_estimate
.saturating_add(other.returned_source_bytes_estimate);
self.matched_file_context_saved_bytes_estimate = self
.matched_file_context_saved_bytes_estimate
.saturating_add(other.matched_file_context_saved_bytes_estimate);
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ContextLogRootSummary {
pub root: String,
pub context_log: String,
pub exists: bool,
pub repositories: Vec<String>,
pub tools: BTreeMap<String, usize>,
#[serde(flatten)]
pub aggregate: ContextLogAggregate,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ContextLogSummary {
pub date_since: String,
pub date_until: String,
pub roots: Vec<ContextLogRootSummary>,
pub totals: ContextLogAggregate,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
struct ContextLogFileSummary {
exists: bool,
repositories: BTreeSet<String>,
tools: BTreeMap<String, usize>,
aggregate: ContextLogAggregate,
}
impl ContextLogFileSummary {
fn into_root_summary(self, root: &Path, context_log: &Path) -> ContextLogRootSummary {
ContextLogRootSummary {
root: root.display().to_string(),
context_log: context_log.display().to_string(),
exists: self.exists,
repositories: self.repositories.into_iter().collect(),
tools: self.tools,
aggregate: self.aggregate,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
struct JsonlSummaryCacheKey {
context_log: PathBuf,
size_bytes: u64,
modified_ns: u128,
date_since: String,
date_until: String,
}
#[derive(Debug, Default)]
struct JsonlSummaryCache {
insertion_order: VecDeque<JsonlSummaryCacheKey>,
entries: BTreeMap<JsonlSummaryCacheKey, ContextLogFileSummary>,
}
impl JsonlSummaryCache {
fn get(&self, key: &JsonlSummaryCacheKey) -> Option<ContextLogFileSummary> {
self.entries.get(key).cloned()
}
fn insert(&mut self, key: JsonlSummaryCacheKey, value: ContextLogFileSummary) {
match self.entries.entry(key) {
Entry::Occupied(mut entry) => {
entry.insert(value);
return;
}
Entry::Vacant(entry) => {
self.insertion_order.push_back(entry.key().clone());
entry.insert(value);
}
}
while self.entries.len() > JSONL_SUMMARY_CACHE_LIMIT {
let Some(oldest_key) = self.insertion_order.pop_front() else {
break;
};
self.entries.remove(&oldest_key);
}
}
}
static JSONL_SUMMARY_CACHE: OnceLock<Mutex<JsonlSummaryCache>> = OnceLock::new();
fn jsonl_summary_cache() -> &'static Mutex<JsonlSummaryCache> {
JSONL_SUMMARY_CACHE.get_or_init(|| Mutex::new(JsonlSummaryCache::default()))
}
static CONTEXT_EFFICIENCY_LOG_APPEND_MUTEX: OnceLock<Mutex<()>> = OnceLock::new();
fn context_efficiency_log_append_mutex() -> &'static Mutex<()> {
CONTEXT_EFFICIENCY_LOG_APPEND_MUTEX.get_or_init(|| Mutex::new(()))
}
impl ContextEfficiencyLogRow {
pub(crate) fn new(
tool: impl Into<String>,
repository_id: impl Into<String>,
snapshot_id: Option<String>,
metrics: ContextEfficiencyLogMetrics,
) -> Self {
Self {
timestamp: Utc::now().to_rfc3339(),
session_id: None,
tool: tool.into(),
repository_id: repository_id.into(),
snapshot_id,
metrics,
}
}
pub(crate) fn with_session_id(mut self, session_id: impl Into<String>) -> Self {
let session_id = session_id.into();
if !session_id.trim().is_empty() {
self.session_id = Some(session_id.trim().to_owned());
}
self
}
}
pub(crate) fn append_context_efficiency_log_row_if_enabled(
workspace_root: &Path,
log_enabled: bool,
row: &ContextEfficiencyLogRow,
) -> io::Result<()> {
if !log_enabled {
return Ok(());
}
append_context_efficiency_log_row(workspace_root, row)
}
pub(crate) fn append_context_efficiency_log_row(
workspace_root: &Path,
row: &ContextEfficiencyLogRow,
) -> io::Result<()> {
let mut encoded = serde_json::to_vec(row).map_err(|error| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("failed to serialize context-efficiency log row: {error}"),
)
})?;
encoded.push(b'\n');
let frigg_dir = workspace_root.join(".frigg");
std::fs::create_dir_all(&frigg_dir)?;
let log_path = frigg_dir.join("context.jsonl");
let _guard = context_efficiency_log_append_mutex()
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let mut file = OpenOptions::new()
.create(true)
.append(true)
.open(log_path)?;
file.write_all(&encoded)?;
Ok(())
}
pub fn summarize_context_logs_for_roots(
roots: &[PathBuf],
window: &ContextSummaryWindow,
) -> Result<ContextLogSummary, ContextSummaryError> {
let mut root_summaries = Vec::new();
let mut totals = ContextLogAggregate::default();
for root in roots {
let context_log = root.join(".frigg/context.jsonl");
let file_summary = summarize_context_log_file(&context_log, window)?;
totals.merge(&file_summary.aggregate);
root_summaries.push(file_summary.into_root_summary(root, &context_log));
}
Ok(ContextLogSummary {
date_since: window.since_key(),
date_until: window.until_key(),
roots: root_summaries,
totals,
})
}
fn summarize_context_log_file(
context_log: &Path,
window: &ContextSummaryWindow,
) -> Result<ContextLogFileSummary, ContextSummaryError> {
let metadata = match std::fs::metadata(context_log) {
Ok(metadata) => metadata,
Err(error) if error.kind() == io::ErrorKind::NotFound => {
return Ok(ContextLogFileSummary::default());
}
Err(error) => return Err(ContextSummaryError::Io(error)),
};
let key = JsonlSummaryCacheKey {
context_log: canonical_context_log_path(context_log),
size_bytes: metadata.len(),
modified_ns: metadata_modified_ns(&metadata),
date_since: window.since_key(),
date_until: window.until_key(),
};
if let Some(summary) = jsonl_summary_cache()
.lock()
.expect("context-efficiency jsonl summary cache mutex poisoned")
.get(&key)
{
return Ok(summary);
}
let summary = read_context_log_file_summary(context_log, window)?;
jsonl_summary_cache()
.lock()
.expect("context-efficiency jsonl summary cache mutex poisoned")
.insert(key, summary.clone());
Ok(summary)
}
fn read_context_log_file_summary(
context_log: &Path,
window: &ContextSummaryWindow,
) -> Result<ContextLogFileSummary, ContextSummaryError> {
let file = File::open(context_log)?;
let mut summary = ContextLogFileSummary {
exists: true,
..ContextLogFileSummary::default()
};
for line in BufReader::new(file).lines() {
let line = line?;
if line.trim().is_empty() {
continue;
}
let Ok(row) = serde_json::from_str::<ContextEfficiencyLogRow>(&line) else {
continue;
};
let Ok(timestamp) = DateTime::parse_from_rfc3339(&row.timestamp) else {
continue;
};
let timestamp = timestamp.with_timezone(&Utc);
if timestamp < window.date_since || timestamp > window.date_until {
continue;
}
summary.repositories.insert(row.repository_id.clone());
*summary.tools.entry(row.tool.clone()).or_default() += 1;
summary.aggregate.add_row(&row);
}
Ok(summary)
}
fn parse_context_summary_datetime(input: &str) -> Result<DateTime<Utc>, String> {
if let Ok(timestamp) = DateTime::parse_from_rfc3339(input) {
return Ok(timestamp.with_timezone(&Utc));
}
let date = NaiveDate::parse_from_str(input, "%Y-%m-%d")
.map_err(|_| format!("expected RFC3339 timestamp or YYYY-MM-DD date, got {input:?}"))?;
let datetime = date
.and_hms_opt(0, 0, 0)
.ok_or_else(|| format!("invalid date: {input:?}"))?;
Ok(DateTime::from_naive_utc_and_offset(datetime, Utc))
}
fn canonical_context_log_path(context_log: &Path) -> PathBuf {
context_log
.canonicalize()
.unwrap_or_else(|_| context_log.to_path_buf())
}
fn metadata_modified_ns(metadata: &std::fs::Metadata) -> u128 {
metadata
.modified()
.ok()
.and_then(|modified| modified.duration_since(UNIX_EPOCH).ok())
.map_or(0, |duration| duration.as_nanos())
}
#[cfg(test)]
pub(crate) fn clear_manifest_metadata_cache_for_tests() {
let mut cache = manifest_metadata_cache()
.lock()
.expect("context-efficiency manifest cache mutex poisoned");
*cache = ManifestMetadataCache::default();
}
#[cfg(test)]
pub(crate) fn manifest_metadata_cache_entry_count_for_tests() -> usize {
manifest_metadata_cache()
.lock()
.expect("context-efficiency manifest cache mutex poisoned")
.entries
.len()
}
#[cfg(test)]
pub(crate) fn clear_jsonl_summary_cache_for_tests() {
let mut cache = jsonl_summary_cache()
.lock()
.expect("context-efficiency jsonl summary cache mutex poisoned");
*cache = JsonlSummaryCache::default();
}
#[cfg(test)]
pub(crate) fn jsonl_summary_cache_entry_count_for_tests() -> usize {
jsonl_summary_cache()
.lock()
.expect("context-efficiency jsonl summary cache mutex poisoned")
.entries
.len()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::storage::{ManifestMetadataEntry, RepositoryManifestMetadataSnapshot};
use std::sync::{Mutex, MutexGuard, OnceLock};
fn context_efficiency_test_lock() -> MutexGuard<'static, ()> {
static TEST_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
TEST_LOCK
.get_or_init(|| Mutex::new(()))
.lock()
.expect("context-efficiency test mutex poisoned")
}
#[test]
fn context_efficiency_guard_requires_response_flag_or_env_log() {
assert!(!need_context_efficiency_with_log_state(None, false));
assert!(!need_context_efficiency_with_log_state(Some(false), false));
assert!(need_context_efficiency_with_log_state(Some(true), false));
assert!(need_context_efficiency_with_log_state(None, true));
assert!(need_context_efficiency_with_log_state(Some(false), true));
}
#[test]
fn context_efficiency_env_truthiness_is_explicitly_false_for_common_false_values() {
for value in ["", "0", "false", "False", " no ", "OFF"] {
assert!(!is_truthy_env_value(value), "{value:?} should be false");
}
for value in ["1", "true", "yes", "on", "anything-else"] {
assert!(is_truthy_env_value(value), "{value:?} should be true");
}
}
#[test]
fn context_efficiency_manifest_summary_derives_totals_and_path_sizes() {
let snapshot = RepositoryManifestMetadataSnapshot {
repository_id: "repo-1".to_owned(),
snapshot_id: "snapshot-1".to_owned(),
entries: vec![
ManifestMetadataEntry {
path: "src/a.rs".to_owned(),
size_bytes: 10,
mtime_ns: Some(300),
},
ManifestMetadataEntry {
path: "src/b.rs".to_owned(),
size_bytes: 25,
mtime_ns: Some(100),
},
ManifestMetadataEntry {
path: "README.md".to_owned(),
size_bytes: 5,
mtime_ns: None,
},
],
};
let summary = ManifestMetadataSummary::from_snapshot(&snapshot);
assert_eq!(summary.indexed_readable_files, 3);
assert_eq!(summary.indexed_readable_bytes, 40);
assert_eq!(summary.min_mtime_ns, Some(100));
assert_eq!(summary.max_mtime_ns, Some(300));
assert_eq!(
summary.returned_unique_file_bytes(["src/a.rs", "src/a.rs", "README.md"]),
15
);
}
#[test]
fn context_efficiency_manifest_cache_is_bounded_fifo() {
let mut cache = ManifestMetadataCache::default();
for index in 0..(MANIFEST_METADATA_CACHE_LIMIT + 3) {
let key = ManifestMetadataCacheKey::new("storage", "repo", format!("snapshot-{index}"));
let summary = ManifestMetadataSummary {
repository_id: "repo".to_owned(),
snapshot_id: format!("snapshot-{index}"),
indexed_readable_files: index,
indexed_readable_bytes: index as u64,
min_mtime_ns: None,
max_mtime_ns: None,
path_size_bytes: BTreeMap::new(),
};
cache.insert(key, summary);
}
assert_eq!(cache.entries.len(), MANIFEST_METADATA_CACHE_LIMIT);
assert!(
cache
.get(&ManifestMetadataCacheKey::new(
"storage",
"repo",
"snapshot-0"
))
.is_none()
);
assert!(
cache
.get(&ManifestMetadataCacheKey::new(
"storage",
"repo",
format!("snapshot-{}", MANIFEST_METADATA_CACHE_LIMIT + 2)
))
.is_some()
);
}
#[test]
fn context_efficiency_jsonl_append_is_env_gated_by_caller() {
let root = std::env::temp_dir().join(format!(
"frigg-context-efficiency-log-disabled-{}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&root);
let row = ContextEfficiencyLogRow::new(
"search_text",
"repo-1",
Some("snapshot-1".to_owned()),
ContextEfficiencyLogMetrics {
indexed_readable_files: 3,
indexed_readable_bytes: 90,
indexed_min_mtime_ns: None,
indexed_max_mtime_ns: None,
candidate_input_count: None,
candidate_output_count: None,
returned_match_count: Some(2),
returned_unique_paths: Some(1),
returned_unique_file_bytes: Some(20),
returned_source_bytes_estimate: Some(5),
matched_file_context_saved_bytes_estimate: Some(15),
matched_file_context_saved_percent_estimate: Some(75.0),
query_duration_ms: Some(4),
narrowing_ratio_estimate: Some(4),
},
);
append_context_efficiency_log_row_if_enabled(&root, false, &row)
.expect("disabled logging should be a no-op");
assert!(!root.join(".frigg/context.jsonl").exists());
let _ = std::fs::remove_dir_all(root);
}
#[test]
fn context_efficiency_jsonl_append_is_compact_and_bounded() {
let root = std::env::temp_dir().join(format!(
"frigg-context-efficiency-log-enabled-{}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&root);
let row = ContextEfficiencyLogRow::new(
"read_file",
"repo-1",
Some("snapshot-1".to_owned()),
ContextEfficiencyLogMetrics {
indexed_readable_files: 2,
indexed_readable_bytes: 140,
indexed_min_mtime_ns: Some(10),
indexed_max_mtime_ns: Some(20),
candidate_input_count: None,
candidate_output_count: None,
returned_match_count: None,
returned_unique_paths: Some(1),
returned_unique_file_bytes: Some(100),
returned_source_bytes_estimate: Some(9),
matched_file_context_saved_bytes_estimate: Some(91),
matched_file_context_saved_percent_estimate: Some(91.0),
query_duration_ms: Some(6),
narrowing_ratio_estimate: Some(11),
},
)
.with_session_id("connected-session-abcdef");
append_context_efficiency_log_row_if_enabled(&root, true, &row)
.expect("enabled logging should append");
let logged = std::fs::read_to_string(root.join(".frigg/context.jsonl"))
.expect("context log should be readable");
assert_eq!(logged.lines().count(), 1);
let value: serde_json::Value =
serde_json::from_str(logged.trim()).expect("context row should be json");
assert_eq!(value["tool"], "read_file");
assert_eq!(value["session_id"], "connected-session-abcdef");
assert_eq!(value["repository_id"], "repo-1");
assert_eq!(value["snapshot_id"], "snapshot-1");
assert_eq!(value["query_duration_ms"], 6);
assert_eq!(value["matched_file_context_saved_bytes_estimate"], 91);
assert_eq!(value["matched_file_context_saved_percent_estimate"], 91.0);
assert_eq!(value["narrowing_ratio_estimate"], 11);
assert!(value.get("corpus_context_saved_bytes_estimate").is_none());
assert!(value.get("corpus_context_saved_percent_estimate").is_none());
assert!(value.get("corpus_narrowing_ratio_estimate").is_none());
assert!(value.get("query").is_none());
assert!(value.get("content").is_none());
assert!(value.get("excerpt").is_none());
let _ = std::fs::remove_dir_all(root);
}
#[test]
fn context_efficiency_jsonl_append_keeps_concurrent_rows_valid() {
let root = std::env::temp_dir().join(format!(
"frigg-context-efficiency-log-concurrent-{}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&root);
let writers: usize = 16;
let writes_per_writer: usize = 4;
let handles = (0..writers)
.map(|writer| {
let root = root.clone();
std::thread::spawn(move || {
for write_index in 0..writes_per_writer {
let row = ContextEfficiencyLogRow::new(
format!("read_file_{writer}"),
format!("repo-{writer}"),
Some(format!("snapshot-{write_index}")),
ContextEfficiencyLogMetrics {
indexed_readable_files: 2,
indexed_readable_bytes: 140,
indexed_min_mtime_ns: Some(10),
indexed_max_mtime_ns: Some(20),
candidate_input_count: None,
candidate_output_count: None,
returned_match_count: None,
returned_unique_paths: Some(1),
returned_unique_file_bytes: Some(100),
returned_source_bytes_estimate: Some(9),
matched_file_context_saved_bytes_estimate: Some(91),
matched_file_context_saved_percent_estimate: Some(91.0),
query_duration_ms: Some(6),
narrowing_ratio_estimate: Some(11),
},
);
append_context_efficiency_log_row(&root, &row)
.expect("concurrent append should succeed");
}
})
})
.collect::<Vec<_>>();
for handle in handles {
handle
.join()
.expect("context-efficiency append thread should not panic");
}
let logged = std::fs::read_to_string(root.join(".frigg/context.jsonl"))
.expect("context log should be readable");
let lines = logged.lines().collect::<Vec<_>>();
assert_eq!(lines.len(), writers * writes_per_writer);
for line in lines {
serde_json::from_str::<ContextEfficiencyLogRow>(line)
.expect("each appended line should remain valid JSON");
}
let _ = std::fs::remove_dir_all(root);
}
#[test]
fn context_efficiency_summary_window_defaults_to_last_30_days() {
let now = DateTime::parse_from_rfc3339("2026-07-02T12:00:00Z")
.expect("timestamp")
.with_timezone(&Utc);
let window =
ContextSummaryWindow::resolve(None, None, now).expect("default window should resolve");
assert_eq!(window.date_since.to_rfc3339(), "2026-06-02T12:00:00+00:00");
assert_eq!(window.date_until.to_rfc3339(), "2026-07-02T12:00:00+00:00");
}
#[test]
fn context_efficiency_summary_window_accepts_date_and_rfc3339_bounds() {
let now = DateTime::parse_from_rfc3339("2026-07-02T12:00:00Z")
.expect("timestamp")
.with_timezone(&Utc);
let until_only = ContextSummaryWindow::resolve(None, Some("2026-07-01T12:00:00Z"), now)
.expect("until-only window should resolve");
assert_eq!(
until_only.date_since.to_rfc3339(),
"2026-06-01T12:00:00+00:00"
);
assert_eq!(
until_only.date_until.to_rfc3339(),
"2026-07-01T12:00:00+00:00"
);
let since_only = ContextSummaryWindow::resolve(Some("2026-06-01"), None, now)
.expect("since-only window should resolve");
assert_eq!(
since_only.date_since.to_rfc3339(),
"2026-06-01T00:00:00+00:00"
);
assert_eq!(
since_only.date_until.to_rfc3339(),
"2026-07-02T12:00:00+00:00"
);
}
#[test]
fn context_efficiency_summary_missing_log_returns_empty_summary() {
let _guard = context_efficiency_test_lock();
clear_jsonl_summary_cache_for_tests();
let root = std::env::temp_dir().join(format!(
"frigg-context-efficiency-summary-missing-{}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&root);
std::fs::create_dir_all(&root).expect("root should be created");
let now = DateTime::parse_from_rfc3339("2026-07-02T12:00:00Z")
.expect("timestamp")
.with_timezone(&Utc);
let window = ContextSummaryWindow::resolve(Some("2026-06-01"), Some("2026-07-01"), now)
.expect("window should resolve");
let summary = summarize_context_logs_for_roots(std::slice::from_ref(&root), &window)
.expect("missing context log should summarize");
assert_eq!(summary.date_since, "2026-06-01T00:00:00+00:00");
assert_eq!(summary.date_until, "2026-07-01T00:00:00+00:00");
assert_eq!(summary.roots.len(), 1);
assert!(!summary.roots[0].exists);
assert_eq!(summary.totals.events, 0);
assert_eq!(jsonl_summary_cache_entry_count_for_tests(), 0);
let _ = std::fs::remove_dir_all(root);
}
#[test]
fn context_efficiency_summary_aggregates_filtered_jsonl_without_raw_rows() {
let _guard = context_efficiency_test_lock();
clear_jsonl_summary_cache_for_tests();
let root = std::env::temp_dir().join(format!(
"frigg-context-efficiency-summary-filtered-{}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&root);
let frigg_dir = root.join(".frigg");
std::fs::create_dir_all(&frigg_dir).expect("frigg dir should be created");
let log_path = frigg_dir.join("context.jsonl");
let rows = [
ContextEfficiencyLogRow {
timestamp: "2026-05-31T23:59:59Z".to_owned(),
session_id: None,
tool: "search_text".to_owned(),
repository_id: "repo-1".to_owned(),
snapshot_id: Some("snapshot-1".to_owned()),
metrics: ContextEfficiencyLogMetrics {
indexed_readable_files: 100,
indexed_readable_bytes: 1000,
indexed_min_mtime_ns: None,
indexed_max_mtime_ns: None,
candidate_input_count: Some(100),
candidate_output_count: Some(10),
returned_match_count: Some(10),
returned_unique_paths: Some(4),
returned_unique_file_bytes: Some(400),
returned_source_bytes_estimate: Some(40),
matched_file_context_saved_bytes_estimate: Some(360),
matched_file_context_saved_percent_estimate: Some(90.0),
query_duration_ms: Some(5),
narrowing_ratio_estimate: Some(10),
},
},
ContextEfficiencyLogRow {
timestamp: "2026-06-10T00:00:00Z".to_owned(),
session_id: Some("session-full-001".to_owned()),
tool: "search_text".to_owned(),
repository_id: "repo-1".to_owned(),
snapshot_id: Some("snapshot-2".to_owned()),
metrics: ContextEfficiencyLogMetrics {
indexed_readable_files: 120,
indexed_readable_bytes: 1200,
indexed_min_mtime_ns: None,
indexed_max_mtime_ns: None,
candidate_input_count: Some(50),
candidate_output_count: Some(5),
returned_match_count: Some(5),
returned_unique_paths: Some(2),
returned_unique_file_bytes: Some(200),
returned_source_bytes_estimate: Some(20),
matched_file_context_saved_bytes_estimate: Some(180),
matched_file_context_saved_percent_estimate: Some(90.0),
query_duration_ms: Some(7),
narrowing_ratio_estimate: Some(10),
},
},
ContextEfficiencyLogRow {
timestamp: "2026-06-11T00:00:00Z".to_owned(),
session_id: Some("session-full-002".to_owned()),
tool: "read_file".to_owned(),
repository_id: "repo-1".to_owned(),
snapshot_id: Some("snapshot-2".to_owned()),
metrics: ContextEfficiencyLogMetrics {
indexed_readable_files: 120,
indexed_readable_bytes: 1200,
indexed_min_mtime_ns: None,
indexed_max_mtime_ns: None,
candidate_input_count: None,
candidate_output_count: None,
returned_match_count: None,
returned_unique_paths: Some(1),
returned_unique_file_bytes: Some(80),
returned_source_bytes_estimate: Some(12),
matched_file_context_saved_bytes_estimate: Some(68),
matched_file_context_saved_percent_estimate: Some(85.0),
query_duration_ms: Some(3),
narrowing_ratio_estimate: Some(7),
},
},
];
let mut encoded = String::new();
for row in rows {
encoded.push_str(&serde_json::to_string(&row).expect("row should serialize"));
encoded.push('\n');
}
std::fs::write(&log_path, encoded).expect("log should be written");
let now = DateTime::parse_from_rfc3339("2026-07-02T12:00:00Z")
.expect("timestamp")
.with_timezone(&Utc);
let window = ContextSummaryWindow::resolve(Some("2026-06-01"), Some("2026-07-01"), now)
.expect("window should resolve");
let summary = summarize_context_logs_for_roots(std::slice::from_ref(&root), &window)
.expect("context log should summarize");
assert_eq!(summary.totals.events, 2);
assert_eq!(summary.totals.indexed_readable_files_max, 120);
assert_eq!(summary.totals.indexed_readable_bytes_max, 1200);
assert_eq!(summary.totals.query_duration_ms_total, 10);
assert_eq!(summary.totals.query_duration_ms_max, 7);
assert_eq!(summary.totals.candidate_input_count, 50);
assert_eq!(summary.totals.returned_unique_file_bytes, 280);
assert_eq!(
summary.totals.matched_file_context_saved_bytes_estimate,
248
);
assert_eq!(summary.roots[0].repositories, vec!["repo-1".to_owned()]);
assert_eq!(summary.roots[0].tools["search_text"], 1);
assert_eq!(summary.roots[0].tools["read_file"], 1);
let value = serde_json::to_value(&summary).expect("summary should serialize");
assert!(value.get("window").is_none());
let serialized = serde_json::to_string(&value).expect("summary should serialize");
assert!(!serialized.contains("2026-06-10T00:00:00Z"));
assert!(!serialized.contains("snapshot-2"));
assert!(!serialized.contains("narrowing_ratio_estimate"));
let _ = std::fs::remove_dir_all(root);
}
#[test]
fn context_efficiency_summary_skips_malformed_jsonl_fragments() {
let _guard = context_efficiency_test_lock();
clear_jsonl_summary_cache_for_tests();
let root = std::env::temp_dir().join(format!(
"frigg-context-efficiency-summary-malformed-{}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&root);
std::fs::create_dir_all(root.join(".frigg")).expect("frigg dir should be created");
let log_path = root.join(".frigg/context.jsonl");
let first = ContextEfficiencyLogRow {
timestamp: "2026-06-10T00:00:00Z".to_owned(),
session_id: None,
tool: "search_text".to_owned(),
repository_id: "repo-1".to_owned(),
snapshot_id: Some("snapshot-1".to_owned()),
metrics: ContextEfficiencyLogMetrics {
indexed_readable_files: 10,
indexed_readable_bytes: 100,
indexed_min_mtime_ns: None,
indexed_max_mtime_ns: None,
candidate_input_count: Some(20),
candidate_output_count: Some(10),
returned_match_count: Some(4),
returned_unique_paths: Some(2),
returned_unique_file_bytes: Some(80),
returned_source_bytes_estimate: Some(30),
matched_file_context_saved_bytes_estimate: Some(50),
matched_file_context_saved_percent_estimate: Some(62.5),
query_duration_ms: Some(7),
narrowing_ratio_estimate: Some(3),
},
};
let second = ContextEfficiencyLogRow {
timestamp: "2026-06-11T00:00:00Z".to_owned(),
session_id: None,
tool: "read_file".to_owned(),
repository_id: "repo-1".to_owned(),
snapshot_id: Some("snapshot-2".to_owned()),
metrics: ContextEfficiencyLogMetrics {
indexed_readable_files: 12,
indexed_readable_bytes: 120,
indexed_min_mtime_ns: None,
indexed_max_mtime_ns: None,
candidate_input_count: None,
candidate_output_count: None,
returned_match_count: None,
returned_unique_paths: Some(1),
returned_unique_file_bytes: Some(40),
returned_source_bytes_estimate: Some(10),
matched_file_context_saved_bytes_estimate: Some(30),
matched_file_context_saved_percent_estimate: Some(75.0),
query_duration_ms: Some(5),
narrowing_ratio_estimate: Some(4),
},
};
let encoded = format!(
"{}\n{{\"timestamp\":\"2026-06-10T00:00:01Z\",\"tool\":\"search_text\"\n:1,\"returned_unique_paths\":1}}\n{}\n",
serde_json::to_string(&first).expect("first row should serialize"),
serde_json::to_string(&second).expect("second row should serialize")
);
std::fs::write(&log_path, encoded).expect("log should be written");
let now = DateTime::parse_from_rfc3339("2026-07-02T12:00:00Z")
.expect("timestamp")
.with_timezone(&Utc);
let window = ContextSummaryWindow::resolve(Some("2026-06-01"), Some("2026-07-01"), now)
.expect("window should resolve");
let summary = summarize_context_logs_for_roots(std::slice::from_ref(&root), &window)
.expect("malformed rows should be skipped");
assert_eq!(summary.totals.events, 2);
assert_eq!(summary.totals.indexed_readable_files_max, 12);
assert_eq!(summary.totals.returned_unique_file_bytes, 120);
assert_eq!(summary.totals.matched_file_context_saved_bytes_estimate, 80);
assert_eq!(summary.roots[0].tools["search_text"], 1);
assert_eq!(summary.roots[0].tools["read_file"], 1);
let _ = std::fs::remove_dir_all(root);
}
#[test]
fn context_efficiency_jsonl_summary_cache_keys_file_metadata_and_window() {
let _guard = context_efficiency_test_lock();
clear_jsonl_summary_cache_for_tests();
let root = std::env::temp_dir().join(format!(
"frigg-context-efficiency-summary-cache-{}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&root);
std::fs::create_dir_all(root.join(".frigg")).expect("frigg dir should be created");
let log_path = root.join(".frigg/context.jsonl");
let row = ContextEfficiencyLogRow {
timestamp: "2026-06-10T00:00:00Z".to_owned(),
session_id: Some("session-full-cache".to_owned()),
tool: "search_text".to_owned(),
repository_id: "repo-1".to_owned(),
snapshot_id: Some("snapshot-1".to_owned()),
metrics: ContextEfficiencyLogMetrics {
indexed_readable_files: 1,
indexed_readable_bytes: 10,
indexed_min_mtime_ns: None,
indexed_max_mtime_ns: None,
candidate_input_count: Some(1),
candidate_output_count: Some(1),
returned_match_count: Some(1),
returned_unique_paths: Some(1),
returned_unique_file_bytes: Some(10),
returned_source_bytes_estimate: Some(5),
matched_file_context_saved_bytes_estimate: Some(5),
matched_file_context_saved_percent_estimate: Some(50.0),
query_duration_ms: Some(1),
narrowing_ratio_estimate: Some(2),
},
};
std::fs::write(
&log_path,
format!(
"{}\n",
serde_json::to_string(&row).expect("row should serialize")
),
)
.expect("log should be written");
let now = DateTime::parse_from_rfc3339("2026-07-02T12:00:00Z")
.expect("timestamp")
.with_timezone(&Utc);
let first_window =
ContextSummaryWindow::resolve(Some("2026-06-01"), Some("2026-07-01"), now)
.expect("first window should resolve");
summarize_context_logs_for_roots(std::slice::from_ref(&root), &first_window)
.expect("first summary should load");
summarize_context_logs_for_roots(std::slice::from_ref(&root), &first_window)
.expect("second summary should use cache");
assert_eq!(jsonl_summary_cache_entry_count_for_tests(), 1);
let second_window =
ContextSummaryWindow::resolve(Some("2026-06-02"), Some("2026-07-01"), now)
.expect("second window should resolve");
summarize_context_logs_for_roots(std::slice::from_ref(&root), &second_window)
.expect("changed window should load");
assert_eq!(jsonl_summary_cache_entry_count_for_tests(), 2);
std::fs::write(&log_path, "").expect("log should be truncated");
summarize_context_logs_for_roots(std::slice::from_ref(&root), &first_window)
.expect("changed file size should load");
assert_eq!(jsonl_summary_cache_entry_count_for_tests(), 3);
let _ = std::fs::remove_dir_all(root);
}
}