Skip to main content

projectatlas_core/
telemetry.rs

1//! Purpose: Track `ProjectAtlas` token savings telemetry.
2
3use crate::outline::estimate_tokens;
4use serde::{Deserialize, Serialize};
5use std::{borrow::Cow, collections::BTreeMap};
6use thiserror::Error;
7
8/// Token overview counting mode.
9pub const TOKEN_ESTIMATE_KIND: &str = "heuristic";
10/// Token overview estimator identifier.
11pub const TOKEN_ESTIMATOR: &str = "chars_or_bytes_div_ceil_4";
12/// Token overview scope label.
13pub const TOKEN_ESTIMATE_SCOPE: &str = "workflow_payload_estimate_not_model_billing_tokens";
14/// Default token-count provider label for offline estimates.
15pub const TOKEN_PROVIDER_HEURISTIC: &str = "heuristic";
16/// Default model label when no model-specific counter is used.
17pub const TOKEN_MODEL_UNKNOWN: &str = "unknown";
18/// Default token-count backend for offline estimates.
19pub const TOKENIZER_BACKEND_HEURISTIC: &str = "chars_div_4";
20/// Accuracy label for the default offline estimator.
21pub const TOKEN_ACCURACY_HEURISTIC: &str = "heuristic_estimate";
22/// Bucket for source compression through summaries, outlines, search, or slices.
23pub const TOKEN_BUCKET_FULL_FILE_COMPRESSION: &str = "full_file_compression";
24/// Bucket for navigation that avoids broad folder/file exploration.
25pub const TOKEN_BUCKET_NAVIGATION_AVOIDANCE: &str = "navigation_avoidance";
26/// Baseline kind for a concrete full-file comparison.
27pub const TOKEN_BASELINE_FULL_FILE: &str = "full_file";
28/// Baseline kind for inferred candidate-set navigation savings.
29pub const TOKEN_BASELINE_SELECTED_CANDIDATES: &str = "selected_candidates";
30/// Baseline kind for broad directory-walk navigation savings.
31pub const TOKEN_BASELINE_DIRECTORY_WALK: &str = "directory_walk";
32/// Fixed average-policy share of a modeled directory-walk baseline.
33pub const TOKEN_AVERAGE_DIRECTORY_WALK_PERCENT: usize = 50;
34/// Evidence label distinguishing the fixed policy from measurement or benchmarking.
35pub const TOKEN_AVERAGE_POLICY_EVIDENCE: &str =
36    "fixed_policy_estimate_not_benchmark_or_provider_measurement";
37/// Evidence label used when predecessor overflow rows lost the folder discriminator.
38pub const TOKEN_AVERAGE_POLICY_OVERFLOW_EVIDENCE: &str =
39    "fixed_policy_estimate_unclassified_overflow_uses_maximum";
40/// Confidence label for observed source-compression comparisons.
41pub const TOKEN_CONFIDENCE_OBSERVED: &str = "observed";
42/// Confidence label for inferred navigation comparisons.
43pub const TOKEN_CONFIDENCE_INFERRED: &str = "inferred";
44/// Confidence label for policy-modeled navigation comparisons.
45pub const TOKEN_CONFIDENCE_POLICY_ESTIMATE: &str = "policy_estimate";
46/// Trace label for the default heuristic calculation.
47pub const TOKEN_TRACE_HEURISTIC: &str = "heuristic=ceil(chars_or_bytes/4)";
48/// Observed before/after accounting layer.
49pub const TOKEN_ACCOUNTING_OBSERVED_DELTA: &str = "observed_delta";
50/// Modeled counterfactual accounting layer.
51pub const TOKEN_ACCOUNTING_MODELED_AVOIDANCE: &str = "modeled_avoidance";
52/// Default method label for heuristic token estimates.
53pub const TOKEN_ESTIMATE_METHOD_HEURISTIC: &str = "heuristic_chars_or_bytes_div_ceil_4";
54/// Dedupe scope for measured one-off events.
55pub const TOKEN_DEDUPE_SCOPE_EVENT: &str = "event";
56/// Dedupe scope for repeated modeled workflow baselines in one session.
57pub const TOKEN_DEDUPE_SCOPE_SESSION: &str = "session";
58/// Read-avoidance confidence for directly observed full-file compression events.
59pub const READ_AVOIDANCE_CONFIDENCE_OBSERVED: &str = "observed";
60/// Read-avoidance confidence for modeled navigation events.
61pub const READ_AVOIDANCE_CONFIDENCE_MODELED: &str = "modeled";
62/// Read-avoidance confidence when raw command evidence is unavailable.
63pub const READ_AVOIDANCE_CONFIDENCE_NOT_RECORDED: &str = "not_recorded";
64/// Human-facing explanation for likely read-avoidance counters.
65pub const READ_AVOIDANCE_SCOPE: &str =
66    "summary_search_slice_calls_that_likely_replaced_whole_file_reads";
67/// CLI command label for file summaries.
68pub const TOKEN_COMMAND_SUMMARY: &str = "summary";
69/// CLI command label for file outlines.
70pub const TOKEN_COMMAND_OUTLINE: &str = "outline";
71/// CLI command label for source slices.
72pub const TOKEN_COMMAND_SLICE: &str = "slice";
73/// CLI command label for symbol slices.
74pub const TOKEN_COMMAND_SYMBOL_SLICE: &str = "symbol-slice";
75/// CLI command label for indexed search.
76pub const TOKEN_COMMAND_SEARCH: &str = "search";
77/// MCP event label for file summaries.
78pub const TOKEN_COMMAND_MCP_FILE_SUMMARY: &str = "mcp.atlas_file_summary";
79/// MCP event label for file outlines.
80pub const TOKEN_COMMAND_MCP_OUTLINE: &str = "mcp.atlas_outline";
81/// MCP event label for source slices.
82pub const TOKEN_COMMAND_MCP_SLICE: &str = "mcp.atlas_slice";
83/// MCP event label for indexed search.
84pub const TOKEN_COMMAND_MCP_SEARCH: &str = "mcp.atlas_search";
85
86/// Typed telemetry-domain validation failure.
87#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
88pub enum TelemetryContractError {
89    /// The all-zero durable runtime identifier is reserved.
90    #[error("the zero usage instance identifier is reserved")]
91    ZeroUsageInstanceId,
92}
93
94/// One bounded CLI invocation or MCP process inside an authoritative project database.
95#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
96pub struct UsageInstanceId([u8; 16]);
97
98impl UsageInstanceId {
99    /// Construct an identity from its durable 16-byte representation.
100    ///
101    /// # Errors
102    ///
103    /// Returns an error for the reserved all-zero value.
104    pub fn from_bytes(bytes: [u8; 16]) -> Result<Self, TelemetryContractError> {
105        if bytes == [0; 16] {
106            return Err(TelemetryContractError::ZeroUsageInstanceId);
107        }
108        Ok(Self(bytes))
109    }
110
111    /// Return the durable 16-byte representation.
112    #[must_use]
113    pub const fn as_bytes(self) -> [u8; 16] {
114        self.0
115    }
116}
117
118/// Runtime owner of one internal telemetry instance.
119#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
120#[serde(rename_all = "snake_case")]
121pub enum UsageInstanceOwner {
122    /// One short-lived command-line invocation.
123    CliInvocation,
124    /// One long-lived MCP server process.
125    McpProcess,
126    /// One direct database-library handle retained for API compatibility.
127    LibraryHandle,
128    /// Historical rows compacted during a supported migration.
129    MigratedLegacy,
130}
131
132impl UsageInstanceOwner {
133    /// Return the stable `SQLite` representation.
134    #[must_use]
135    pub const fn as_str(self) -> &'static str {
136        match self {
137            Self::CliInvocation => "cli_invocation",
138            Self::McpProcess => "mcp_process",
139            Self::LibraryHandle => "library_handle",
140            Self::MigratedLegacy => "migrated_legacy",
141        }
142    }
143
144    /// Parse the stable `SQLite` representation.
145    #[must_use]
146    pub fn parse(value: &str) -> Option<Self> {
147        match value {
148            "cli_invocation" => Some(Self::CliInvocation),
149            "mcp_process" => Some(Self::McpProcess),
150            "library_handle" => Some(Self::LibraryHandle),
151            "migrated_legacy" => Some(Self::MigratedLegacy),
152            _ => None,
153        }
154    }
155}
156
157/// Truth state for caller-label and raw telemetry detail.
158#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
159#[serde(rename_all = "snake_case")]
160pub enum UsageDetailAvailability {
161    /// Aggregate and retained recent detail are complete for the requested scope.
162    Retained,
163    /// Numeric aggregates remain available but some detail or dimensions were compacted.
164    Partial,
165    /// A bounded tombstone proves the requested label existed but its report expired.
166    Expired,
167    /// No retained aggregate or tombstone can establish the requested scope.
168    #[default]
169    Unavailable,
170}
171
172impl UsageDetailAvailability {
173    /// Return the stable serialized label.
174    #[must_use]
175    pub const fn as_str(self) -> &'static str {
176        match self {
177            Self::Retained => "retained",
178            Self::Partial => "partial",
179            Self::Expired => "expired",
180            Self::Unavailable => "unavailable",
181        }
182    }
183
184    /// Parse the stable `SQLite` representation.
185    #[must_use]
186    pub fn parse(value: &str) -> Option<Self> {
187        match value {
188            "retained" => Some(Self::Retained),
189            "partial" => Some(Self::Partial),
190            "expired" => Some(Self::Expired),
191            "unavailable" => Some(Self::Unavailable),
192            _ => None,
193        }
194    }
195}
196
197/// Wide separated accounting totals before narrowing to the public report representation.
198#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
199pub struct TokenAccountingTotals {
200    /// Observed before/after saved tokens.
201    pub measured_tokens_saved: i128,
202    /// Gross modeled avoided tokens before baseline deduplication.
203    pub gross_modeled_tokens_avoided: i128,
204    /// Modeled avoided tokens after runtime-instance baseline deduplication.
205    pub deduped_modeled_tokens_avoided: i128,
206    /// Average-policy modeled avoided tokens after baseline deduplication.
207    pub average_modeled_tokens_avoided: i128,
208    /// Number of repeated modeled baseline calls collapsed by deduplication.
209    pub repeated_baselines_deduped: u128,
210    /// Observed calls that replaced a whole-file read.
211    pub observed_file_read_replacements: u128,
212    /// Modeled navigation calls that likely avoided a whole-file read.
213    pub modeled_file_reads_avoided: u128,
214}
215
216/// Token savings event for a funnel command.
217#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
218pub struct UsageEvent {
219    /// Optional caller-visible compatibility label, distinct from runtime identity.
220    pub session_id: String,
221    /// Command or tool name.
222    pub command: String,
223    /// Optional path affected by the command.
224    pub path: Option<String>,
225    /// Optional query text.
226    pub query: Option<String>,
227    /// Baseline token estimate without `ProjectAtlas`.
228    pub estimated_tokens_without_projectatlas: Option<usize>,
229    /// Actual token estimate with `ProjectAtlas`.
230    pub estimated_tokens_with_projectatlas: Option<usize>,
231    /// Estimated token delta.
232    pub estimated_tokens_saved: Option<isize>,
233    /// Savings bucket used for reporting hard evidence separately from modeled savings.
234    #[serde(default = "default_token_savings_bucket")]
235    pub token_savings_bucket: String,
236    /// Provider used for token counting.
237    #[serde(default = "default_token_provider")]
238    pub provider: String,
239    /// Model used for token counting.
240    #[serde(default = "default_token_model")]
241    pub model: String,
242    /// Tokenizer or API backend used for token counting.
243    #[serde(default = "default_tokenizer_backend")]
244    pub tokenizer_backend: String,
245    /// Accuracy level for the token count.
246    #[serde(default = "default_token_accuracy")]
247    pub accuracy: String,
248    /// Baseline scenario used for the without-ProjectAtlas estimate.
249    #[serde(default = "default_token_baseline_kind")]
250    pub baseline_kind: String,
251    /// Confidence level for the baseline scenario.
252    #[serde(default = "default_token_confidence")]
253    pub confidence: String,
254    /// Compact calculation trace.
255    #[serde(default = "default_token_trace")]
256    pub calculation_trace: String,
257    /// Accounting layer used to separate measured deltas from modeled avoidance.
258    #[serde(default = "default_accounting_layer")]
259    pub accounting_layer: String,
260    /// Token estimate method used for this event.
261    #[serde(default = "default_estimate_method")]
262    pub estimate_method: String,
263    /// Denominator represented by the baseline estimate.
264    #[serde(default = "default_denominator_kind")]
265    pub denominator_kind: String,
266    /// Stable modeled-baseline identity for deduplication.
267    #[serde(default)]
268    pub baseline_identity: String,
269    /// Stable modeled-baseline fingerprint for deduplication.
270    #[serde(default)]
271    pub baseline_fingerprint: String,
272    /// Scope used when deduplicating modeled avoidance.
273    #[serde(default = "default_dedupe_scope")]
274    pub dedupe_scope: String,
275}
276
277/// Aggregated token savings for one bucket and counting mode.
278#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
279pub struct TokenBucketOverview {
280    /// Savings bucket.
281    pub token_savings_bucket: String,
282    /// Provider used for token counting.
283    pub provider: String,
284    /// Model used for token counting.
285    pub model: String,
286    /// Tokenizer or API backend used for token counting.
287    pub tokenizer_backend: String,
288    /// Accuracy level for the token count.
289    pub accuracy: String,
290    /// Baseline scenario used for the without-ProjectAtlas estimate.
291    pub baseline_kind: String,
292    /// Confidence level for the baseline scenario.
293    pub confidence: String,
294    /// Number of tracked calls in this bucket.
295    pub calls: usize,
296    /// Total baseline estimate.
297    pub estimated_without_projectatlas: usize,
298    /// Total `ProjectAtlas` estimate.
299    pub estimated_with_projectatlas: usize,
300    /// Total saved tokens.
301    pub estimated_saved: isize,
302    /// Signed savings ratio, or `None` when the baseline estimate is zero.
303    pub savings_rate: Option<f64>,
304    /// Accounting layer used to separate measured deltas from modeled avoidance.
305    pub accounting_layer: String,
306    /// Token estimate method used for this bucket.
307    pub estimate_method: String,
308    /// Denominator represented by the baseline estimate.
309    pub denominator_kind: String,
310    /// Dedupe scope used by events in this bucket.
311    pub dedupe_scope: String,
312}
313
314/// Optional local tokenizer calibration for indexed UTF-8 files.
315#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
316pub struct TokenCalibrationOverview {
317    /// Tokenizer name.
318    pub tokenizer: String,
319    /// Provider label.
320    pub provider: String,
321    /// Model label.
322    pub model: String,
323    /// Tokenizer backend label.
324    pub tokenizer_backend: String,
325    /// Accuracy label.
326    pub accuracy: String,
327    /// Indexed UTF-8 file count.
328    pub files: usize,
329    /// Indexed UTF-8 byte count.
330    pub bytes: usize,
331    /// Existing heuristic estimate over indexed UTF-8 files.
332    pub heuristic_tokens: usize,
333    /// Local tokenizer count over indexed UTF-8 files.
334    pub calibrated_tokens: usize,
335    /// Heuristic-to-calibrated ratio, or `None` when calibrated count is zero.
336    pub heuristic_to_calibrated_ratio: Option<f64>,
337}
338
339/// Validation state for optional agent-efficiency benchmark evidence.
340#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
341#[serde(rename_all = "snake_case")]
342pub enum AgentEfficiencyEvidenceState {
343    /// No benchmark artifact was requested.
344    #[default]
345    Unavailable,
346    /// The requested artifact could not be read or decoded safely.
347    Failed,
348    /// The artifact decoded but does not match the supported release contract.
349    Incompatible,
350    /// Some matched evidence is valid while retained failures remain explicit.
351    Partial,
352    /// All required candidate and baseline trials matched successfully.
353    Compatible,
354}
355
356impl AgentEfficiencyEvidenceState {
357    /// Return the stable serialized label.
358    #[must_use]
359    pub const fn as_str(self) -> &'static str {
360        match self {
361            Self::Unavailable => "unavailable",
362            Self::Failed => "failed",
363            Self::Incompatible => "incompatible",
364            Self::Partial => "partial",
365            Self::Compatible => "compatible",
366        }
367    }
368}
369
370/// Baseline arm compared with the `v0.4` candidate.
371#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
372#[serde(rename_all = "snake_case")]
373pub enum AgentEfficiencyBaseline {
374    /// Frozen `ProjectAtlas` `v0.3.26` runtime and packaged skill.
375    FrozenProjectAtlasV0326,
376    /// Codex navigation without `ProjectAtlas`.
377    PlainCodex,
378}
379
380/// Identity retained from one validated benchmark artifact.
381#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
382pub struct AgentEfficiencyArtifactIdentity {
383    /// Supported benchmark schema version.
384    pub schema_version: u32,
385    /// Digest algorithm used for `artifact_digest`.
386    pub artifact_digest_kind: String,
387    /// Digest of the exact validated artifact bytes.
388    pub artifact_digest: String,
389    /// Candidate runtime semantic version.
390    pub candidate_version: String,
391    /// Candidate runtime SHA-256 identity.
392    pub candidate_runtime_sha256: String,
393    /// Descriptive source checkout commit recorded by the benchmark.
394    #[serde(default)]
395    pub candidate_source_head: String,
396    /// Compatibility identity key; descriptive only and mirrors `candidate_source_head`.
397    #[serde(default)]
398    pub candidate_functional_head: String,
399    /// Compatibility identity key; descriptive only and mirrors `candidate_source_head`.
400    #[serde(default)]
401    pub candidate_checklist_head: String,
402    /// Frozen `ProjectAtlas` runtime semantic version.
403    pub frozen_version: String,
404    /// Frozen `ProjectAtlas` runtime `SHA-256` identity.
405    pub frozen_runtime_sha256: String,
406}
407
408/// Closed navigation metric projected from matched benchmark trials.
409#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
410#[serde(rename_all = "snake_case")]
411pub enum AgentEfficiencyMetricKind {
412    /// All tool calls made by the agent.
413    TotalToolCalls,
414    /// Calls made through the `ProjectAtlas` MCP server.
415    ProjectAtlasCalls,
416    /// Productive folder selections.
417    ProductiveFolders,
418    /// Productive file selections.
419    ProductiveFiles,
420    /// Productive relation selections.
421    ProductiveRelations,
422    /// Wrong folder selections.
423    WrongFolders,
424    /// Wrong file selections.
425    WrongFiles,
426    /// Wrong relation selections.
427    WrongRelations,
428    /// Broad source reads.
429    BroadReads,
430    /// Full source-file reads.
431    FullReads,
432    /// Navigation backtracks.
433    Backtracks,
434    /// Gross navigation-context bytes.
435    GrossNavigationBytes,
436    /// Net navigation-context bytes including setup material.
437    NetNavigationBytes,
438    /// Gross navigation-context heuristic tokens.
439    GrossNavigationTokens,
440    /// Net navigation-context heuristic tokens including setup material.
441    NetNavigationTokens,
442    /// Candidate setup wall time.
443    SetupWallSeconds,
444    /// Per-task runtime wall time after setup.
445    RuntimeWallSeconds,
446    /// Persistent bytes retained after the trial.
447    PersistentBytes,
448}
449
450/// Candidate and baseline distribution summary for one navigation metric.
451#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
452pub struct AgentEfficiencyMetricComparison {
453    /// Metric represented by this row.
454    pub metric: AgentEfficiencyMetricKind,
455    /// Median across matched candidate trials.
456    pub candidate_median: f64,
457    /// Median across matched baseline trials.
458    pub baseline_median: f64,
459    /// Observed maximum across matched candidate trials.
460    pub candidate_maximum: f64,
461    /// Observed maximum across matched baseline trials.
462    pub baseline_maximum: f64,
463    /// Lower-is-better median percentage saving, absent for a zero denominator.
464    pub median_percent_saving: Option<f64>,
465}
466
467/// Workload-specific setup/runtime break-even truth.
468#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
469pub struct AgentEfficiencyBreakEven {
470    /// Validated benchmark workload name.
471    pub workload: String,
472    /// Tasks required to repay setup wall time, or `None` when no positive saving exists.
473    pub wall_time_tasks: Option<u64>,
474}
475
476/// Provider counter represented only as descriptive benchmark context.
477#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
478#[serde(rename_all = "snake_case")]
479pub enum AgentEfficiencyProviderMetricKind {
480    /// Provider input-token counter.
481    InputTokens,
482    /// Provider cached-input-token counter.
483    CachedInputTokens,
484    /// Provider cache-write input-token counter.
485    CacheWriteInputTokens,
486    /// Provider output-token counter.
487    OutputTokens,
488    /// Provider reasoning-output-token counter.
489    ReasoningOutputTokens,
490}
491
492/// Descriptive-only candidate and baseline provider counter.
493#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
494pub struct AgentEfficiencyProviderMetric {
495    /// Provider counter represented by this row.
496    pub metric: AgentEfficiencyProviderMetricKind,
497    /// Candidate median reported by the provider.
498    pub candidate_median: f64,
499    /// Baseline median reported by the provider.
500    pub baseline_median: f64,
501    /// Candidate observed maximum reported by the provider.
502    pub candidate_maximum: f64,
503    /// Baseline observed maximum reported by the provider.
504    pub baseline_maximum: f64,
505    /// Always false because provider counters do not prove navigation causality.
506    pub causal_attribution: bool,
507}
508
509/// One matched baseline comparison projected from the benchmark artifact.
510#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
511pub struct AgentEfficiencyBaselineRow {
512    /// Compared baseline arm.
513    pub baseline: AgentEfficiencyBaseline,
514    /// Evidence state for this baseline.
515    pub state: AgentEfficiencyEvidenceState,
516    /// Candidate and baseline trials that completed the same workload and repeat.
517    pub matched_trials: usize,
518    /// Failed candidate trials retained outside matched denominators.
519    pub candidate_failed_trials: usize,
520    /// Failed baseline trials retained outside matched denominators.
521    pub baseline_failed_trials: usize,
522    /// Completed trials without a completed counterpart.
523    pub unmatched_trials: usize,
524    /// Bounded matched navigation distributions.
525    pub metrics: Vec<AgentEfficiencyMetricComparison>,
526    /// Workload-specific setup/runtime break-even truth.
527    pub break_even: Vec<AgentEfficiencyBreakEven>,
528    /// Provider counters retained as descriptive-only context.
529    pub provider_usage_descriptive_only: Vec<AgentEfficiencyProviderMetric>,
530}
531
532/// Durable `ProjectAtlas` navigation capability represented in the benchmark.
533#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
534#[serde(rename_all = "snake_case")]
535pub enum AgentEfficiencyCapability {
536    /// Initial project, purpose, and connection discovery.
537    Discovery,
538    /// Summary, outline, and exact-slice compression.
539    SummaryAndSlice,
540    /// Lexical search narrowing.
541    Search,
542    /// Symbol and relation navigation.
543    SymbolsAndRelations,
544    /// Trace-completed `ProjectAtlas` calls outside the supported named groups.
545    Other,
546}
547
548/// Trace-completed `v0.4` MCP calls grouped by navigation responsibility.
549#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
550pub struct AgentEfficiencyCapabilityContribution {
551    /// Capability responsibility represented by this row.
552    pub capability: AgentEfficiencyCapability,
553    /// Trace-completed `ProjectAtlas` MCP calls.
554    pub calls: usize,
555    /// Bytes emitted by those MCP calls.
556    pub emitted_bytes: u64,
557}
558
559/// Optional controlled benchmark comparison attached to live token telemetry.
560#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
561pub struct AgentEfficiencyComparison {
562    /// Overall evidence state.
563    pub state: AgentEfficiencyEvidenceState,
564    /// Bounded explanation for unavailable, failed, incompatible, or partial evidence.
565    pub reason: Option<String>,
566    /// Validated artifact and runtime identity.
567    pub artifact: Option<AgentEfficiencyArtifactIdentity>,
568    /// Frozen-v0.3.26 and plain-control rows.
569    pub baselines: Vec<AgentEfficiencyBaselineRow>,
570    /// Trace-completed candidate MCP calls grouped without causal token attribution.
571    pub capabilities: Vec<AgentEfficiencyCapabilityContribution>,
572    /// Whether provider counters are explicitly non-causal.
573    pub provider_counters_descriptive_only: bool,
574}
575
576impl Default for AgentEfficiencyComparison {
577    fn default() -> Self {
578        Self {
579            state: AgentEfficiencyEvidenceState::Unavailable,
580            reason: Some("benchmark artifact not supplied".to_string()),
581            artifact: None,
582            baselines: Vec::new(),
583            capabilities: Vec::new(),
584            provider_counters_descriptive_only: true,
585        }
586    }
587}
588
589/// Fixed policy metadata for the primary average token estimate.
590#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
591pub struct TokenAveragePolicy {
592    /// Share of each retained folder-scope baseline admitted to the average estimate.
593    pub directory_walk_baseline_percent: usize,
594    /// Share of the actual `ProjectAtlas` payload charged to the estimate.
595    pub atlas_payload_percent: usize,
596    /// Evidence classification for the estimate.
597    pub evidence: String,
598}
599
600impl Default for TokenAveragePolicy {
601    fn default() -> Self {
602        Self {
603            directory_walk_baseline_percent: TOKEN_AVERAGE_DIRECTORY_WALK_PERCENT,
604            atlas_payload_percent: 100,
605            evidence: TOKEN_AVERAGE_POLICY_EVIDENCE.to_string(),
606        }
607    }
608}
609
610/// Token savings overview.
611#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
612pub struct TokenOverview {
613    /// Counting mode for the reported numbers.
614    pub estimate_kind: String,
615    /// Estimator used to produce the reported numbers.
616    pub estimator: String,
617    /// Scope and accuracy boundary for the reported numbers.
618    pub estimate_scope: String,
619    /// Number of tracked calls.
620    pub calls: usize,
621    /// Total baseline estimate.
622    pub estimated_without_projectatlas: usize,
623    /// Total `ProjectAtlas` estimate.
624    pub estimated_with_projectatlas: usize,
625    /// Total saved tokens.
626    pub estimated_saved: isize,
627    /// Signed savings ratio, or `None` when the baseline estimate is zero.
628    pub savings_rate: Option<f64>,
629    /// Bucketed token savings grouped by baseline and accuracy semantics.
630    pub buckets: Vec<TokenBucketOverview>,
631    /// Observed before/after saved tokens.
632    pub measured_tokens_saved: isize,
633    /// Gross modeled avoided-token estimate before dedupe.
634    pub gross_modeled_tokens_avoided: isize,
635    /// Deduped modeled avoided-token estimate.
636    pub deduped_modeled_tokens_avoided: isize,
637    /// Average-policy modeled avoided-token estimate.
638    #[serde(default)]
639    pub average_modeled_tokens_avoided: isize,
640    /// Explicit average-policy tokens avoided estimate.
641    #[serde(default)]
642    pub average_tokens_avoided: isize,
643    /// Explicit all-files maximum tokens avoided estimate.
644    #[serde(default)]
645    pub maximum_tokens_avoided: isize,
646    /// Fixed policy metadata shared by JSON, TOON, CLI, and MCP reports.
647    #[serde(default)]
648    pub average_policy: TokenAveragePolicy,
649    /// Primary compatibility alias for `average_tokens_avoided`.
650    pub tokens_avoided: isize,
651    /// Legacy all-bucket gross estimate retained for migration diagnostics.
652    pub legacy_gross_estimated_saved: isize,
653    /// Number of duplicate modeled baseline events collapsed by dedupe.
654    pub repeated_baselines_deduped: usize,
655    /// Observed `ProjectAtlas` summary/search/slice calls compared with whole-file reads.
656    #[serde(default)]
657    pub observed_file_read_replacements: usize,
658    /// Modeled `ProjectAtlas` navigation calls that likely avoided whole-file reads.
659    #[serde(default)]
660    pub modeled_file_reads_avoided: usize,
661    /// Total likely whole-file reads avoided.
662    #[serde(default)]
663    pub likely_file_reads_avoided: usize,
664    /// Scope label for read-avoidance counters.
665    #[serde(default = "default_read_avoidance_scope")]
666    pub read_avoidance_scope: String,
667    /// Confidence label for read-avoidance counters.
668    #[serde(default = "default_read_avoidance_confidence")]
669    pub read_avoidance_confidence: String,
670    /// Optional local tokenizer calibration for indexed UTF-8 files.
671    pub calibration: Option<TokenCalibrationOverview>,
672    /// Availability of caller-label and retained raw detail for this report.
673    #[serde(default)]
674    pub detail_availability: UsageDetailAvailability,
675    /// Optional validated controlled benchmark evidence kept separate from live accounting.
676    #[serde(default)]
677    pub agent_efficiency: AgentEfficiencyComparison,
678}
679
680/// Token trend grouping window.
681#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
682#[serde(rename_all = "lowercase")]
683pub enum TokenTrendWindow {
684    /// Group token telemetry by day.
685    Day,
686    /// Group token telemetry by week.
687    Week,
688    /// Group token telemetry by month.
689    Month,
690    /// Group token telemetry by year.
691    Year,
692}
693
694impl TokenTrendWindow {
695    /// Parse a stable window label.
696    #[must_use]
697    pub fn parse(value: &str) -> Option<Self> {
698        match value {
699            "day" => Some(Self::Day),
700            "week" => Some(Self::Week),
701            "month" => Some(Self::Month),
702            "year" => Some(Self::Year),
703            _ => None,
704        }
705    }
706
707    /// Return the stable CLI/MCP label.
708    #[must_use]
709    pub const fn as_str(self) -> &'static str {
710        match self {
711            Self::Day => "day",
712            Self::Week => "week",
713            Self::Month => "month",
714            Self::Year => "year",
715        }
716    }
717}
718
719impl std::fmt::Display for TokenTrendWindow {
720    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
721        formatter.write_str(self.as_str())
722    }
723}
724
725/// Token trend aggregate for one period.
726#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
727pub struct TokenTrendPeriod {
728    /// Period label such as `2026-06-29`, `2026-W26`, `2026-06`, or `2026`.
729    pub period: String,
730    /// Number of tracked calls in the period.
731    pub calls: usize,
732    /// Total baseline estimate.
733    pub estimated_without_projectatlas: usize,
734    /// Total `ProjectAtlas` estimate.
735    pub estimated_with_projectatlas: usize,
736    /// Total saved tokens.
737    pub estimated_saved: isize,
738    /// Signed savings ratio, or `None` when the baseline estimate is zero.
739    pub savings_rate: Option<f64>,
740    /// Bucketed token savings grouped by baseline and accuracy semantics.
741    pub buckets: Vec<TokenBucketOverview>,
742}
743
744/// Token savings trend report.
745#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
746pub struct TokenTrendReport {
747    /// Counting mode for the reported numbers.
748    pub estimate_kind: String,
749    /// Estimator used to produce the reported numbers.
750    pub estimator: String,
751    /// Scope and accuracy boundary for the reported numbers.
752    pub estimate_scope: String,
753    /// Optional caller-visible compatibility-label filter.
754    pub session: Option<String>,
755    /// Grouping window.
756    pub window: TokenTrendWindow,
757    /// Period aggregates ordered oldest to newest.
758    pub periods: Vec<TokenTrendPeriod>,
759    /// Availability of the requested retained trend scope.
760    #[serde(default)]
761    pub detail_availability: UsageDetailAvailability,
762}
763
764impl UsageEvent {
765    /// Return whether this event represents an observed before/after source comparison.
766    #[must_use]
767    pub fn is_observed(&self) -> bool {
768        is_observed_event(self)
769    }
770
771    /// Return whether this event represents modeled navigation avoidance.
772    #[must_use]
773    pub fn is_modeled(&self) -> bool {
774        is_modeled_event(self)
775    }
776
777    /// Return whether this observed event is strong whole-file replacement evidence.
778    #[must_use]
779    pub fn is_observed_file_read_replacement(&self, baseline_tokens: usize) -> bool {
780        is_observed_read_replacement_event(self, baseline_tokens)
781    }
782
783    /// Return whether this modeled event is strong whole-file avoidance evidence.
784    #[must_use]
785    pub fn is_modeled_file_read_avoidance(&self, baseline_tokens: usize) -> bool {
786        is_modeled_read_avoidance_event(self, baseline_tokens)
787    }
788
789    /// Return the normalized accounting layer used by bucket reports.
790    #[must_use]
791    pub fn report_accounting_layer(&self) -> &str {
792        if self.is_observed() {
793            TOKEN_ACCOUNTING_OBSERVED_DELTA
794        } else {
795            &self.accounting_layer
796        }
797    }
798
799    /// Return the normalized denominator used by bucket reports.
800    #[must_use]
801    pub fn report_denominator_kind(&self) -> &str {
802        if self.is_observed() {
803            TOKEN_BASELINE_FULL_FILE
804        } else {
805            &self.denominator_kind
806        }
807    }
808
809    /// Return the normalized deduplication scope used by bucket reports.
810    #[must_use]
811    pub fn report_dedupe_scope(&self) -> &str {
812        if self.is_observed() {
813            TOKEN_DEDUPE_SCOPE_EVENT
814        } else {
815            &self.dedupe_scope
816        }
817    }
818
819    /// Return the modeled baseline identity, including the legacy fallback.
820    #[must_use]
821    pub fn effective_baseline_identity(&self) -> Cow<'_, str> {
822        if self.baseline_identity.is_empty() {
823            Cow::Owned(default_baseline_identity(
824                &self.command,
825                self.path.as_deref(),
826                self.query.as_deref(),
827                &self.baseline_kind,
828            ))
829        } else {
830            Cow::Borrowed(&self.baseline_identity)
831        }
832    }
833
834    /// Return the modeled baseline fingerprint, including the legacy fallback.
835    #[must_use]
836    pub fn effective_baseline_fingerprint(&self) -> Cow<'_, str> {
837        if self.baseline_fingerprint.is_empty() {
838            self.effective_baseline_identity()
839        } else {
840            Cow::Borrowed(&self.baseline_fingerprint)
841        }
842    }
843
844    /// Return the fixed collision-resistant key for one modeled baseline witness.
845    #[must_use]
846    pub fn modeled_baseline_key(&self) -> [u8; 32] {
847        let identity = self.effective_baseline_identity();
848        let fingerprint = if self.baseline_fingerprint.is_empty() {
849            identity.as_ref()
850        } else {
851            self.baseline_fingerprint.as_str()
852        };
853        let mut hasher = blake3::Hasher::new();
854        for value in [
855            identity.as_ref(),
856            fingerprint,
857            self.denominator_kind.as_str(),
858        ] {
859            let bytes = value.as_bytes();
860            hasher.update(&(bytes.len() as u64).to_le_bytes());
861            hasher.update(bytes);
862        }
863        *hasher.finalize().as_bytes()
864    }
865}
866
867impl TokenOverview {
868    /// Build an overview from usage events.
869    #[must_use]
870    pub fn from_events(events: &[UsageEvent]) -> Self {
871        let mut totals = BTreeMap::<TokenBucketKey, (u128, u128, u128)>::new();
872        for event in events {
873            let (Some(event_without), Some(event_with)) = (
874                event.estimated_tokens_without_projectatlas,
875                event.estimated_tokens_with_projectatlas,
876            ) else {
877                continue;
878            };
879            let entry = totals.entry(TokenBucketKey::from(event)).or_default();
880            entry.0 = entry.0.saturating_add(1);
881            entry.1 = entry.1.saturating_add(event_without as u128);
882            entry.2 = entry.2.saturating_add(event_with as u128);
883        }
884        let buckets = totals
885            .into_iter()
886            .map(|(key, (calls, without, with))| key.into_overview(calls, without, with))
887            .collect();
888        let mut overview = Self::from_buckets(buckets);
889        overview.apply_accounting_from_events(events);
890        overview
891    }
892
893    /// Build an overview from aggregate heuristic token totals.
894    #[must_use]
895    pub fn from_estimated_totals(calls: u128, without: u128, with: u128) -> Self {
896        Self::from_buckets(vec![TokenBucketOverview::from_totals(
897            default_token_savings_bucket(),
898            default_token_provider(),
899            default_token_model(),
900            default_tokenizer_backend(),
901            default_token_accuracy(),
902            default_token_baseline_kind(),
903            default_token_confidence(),
904            default_accounting_layer(),
905            default_estimate_method(),
906            default_denominator_kind(),
907            default_dedupe_scope(),
908            calls,
909            without,
910            with,
911        )])
912    }
913
914    /// Build an overview from pre-aggregated buckets.
915    #[must_use]
916    pub fn from_buckets(buckets: Vec<TokenBucketOverview>) -> Self {
917        let calls = buckets.iter().fold(0u128, |acc, bucket| {
918            acc.saturating_add(bucket.calls as u128)
919        });
920        let without = buckets.iter().fold(0u128, |acc, bucket| {
921            acc.saturating_add(bucket.estimated_without_projectatlas as u128)
922        });
923        let with = buckets.iter().fold(0u128, |acc, bucket| {
924            acc.saturating_add(bucket.estimated_with_projectatlas as u128)
925        });
926        let saved = aggregate_token_delta(without, with);
927        let savings_rate = if without == 0 {
928            None
929        } else {
930            Some((without as f64 - with as f64) / without as f64)
931        };
932        let measured_tokens_saved_wide = measured_tokens_saved_from_buckets(&buckets);
933        let gross_modeled_tokens_avoided_wide = modeled_tokens_saved_from_buckets(&buckets);
934        let average_modeled_tokens_avoided_wide =
935            average_modeled_tokens_saved_from_buckets(&buckets);
936        let measured_tokens_saved = saturating_i128_to_isize(measured_tokens_saved_wide);
937        let gross_modeled_tokens_avoided =
938            saturating_i128_to_isize(gross_modeled_tokens_avoided_wide);
939        let average_modeled_tokens_avoided =
940            saturating_i128_to_isize(average_modeled_tokens_avoided_wide);
941        let average_tokens_avoided = saturating_i128_to_isize(
942            measured_tokens_saved_wide.saturating_add(average_modeled_tokens_avoided_wide),
943        );
944        let maximum_tokens_avoided = saturating_i128_to_isize(
945            measured_tokens_saved_wide.saturating_add(gross_modeled_tokens_avoided_wide),
946        );
947        Self {
948            estimate_kind: TOKEN_ESTIMATE_KIND.to_string(),
949            estimator: TOKEN_ESTIMATOR.to_string(),
950            estimate_scope: TOKEN_ESTIMATE_SCOPE.to_string(),
951            calls: saturating_u128_to_usize(calls),
952            estimated_without_projectatlas: saturating_u128_to_usize(without),
953            estimated_with_projectatlas: saturating_u128_to_usize(with),
954            estimated_saved: saved,
955            savings_rate,
956            measured_tokens_saved,
957            gross_modeled_tokens_avoided,
958            deduped_modeled_tokens_avoided: gross_modeled_tokens_avoided,
959            average_modeled_tokens_avoided,
960            average_tokens_avoided,
961            maximum_tokens_avoided,
962            average_policy: TokenAveragePolicy::default(),
963            tokens_avoided: average_tokens_avoided,
964            legacy_gross_estimated_saved: saved,
965            repeated_baselines_deduped: 0,
966            observed_file_read_replacements: 0,
967            modeled_file_reads_avoided: 0,
968            likely_file_reads_avoided: 0,
969            read_avoidance_scope: READ_AVOIDANCE_SCOPE.to_string(),
970            read_avoidance_confidence: READ_AVOIDANCE_CONFIDENCE_NOT_RECORDED.to_string(),
971            calibration: None,
972            detail_availability: UsageDetailAvailability::Retained,
973            agent_efficiency: AgentEfficiencyComparison::default(),
974            buckets,
975        }
976    }
977
978    /// Attach a local tokenizer calibration section.
979    pub fn set_calibration(&mut self, calibration: TokenCalibrationOverview) {
980        self.calibration = Some(calibration);
981    }
982
983    /// Attach one validated controlled benchmark comparison.
984    pub fn set_agent_efficiency(&mut self, comparison: AgentEfficiencyComparison) {
985        self.agent_efficiency = comparison;
986    }
987
988    /// Apply exact separated accounting totals loaded from durable aggregates.
989    pub fn apply_accounting_totals(&mut self, totals: TokenAccountingTotals) {
990        self.measured_tokens_saved = saturating_i128_to_isize(totals.measured_tokens_saved);
991        self.gross_modeled_tokens_avoided =
992            saturating_i128_to_isize(totals.gross_modeled_tokens_avoided);
993        self.deduped_modeled_tokens_avoided =
994            saturating_i128_to_isize(totals.deduped_modeled_tokens_avoided);
995        self.average_modeled_tokens_avoided =
996            saturating_i128_to_isize(totals.average_modeled_tokens_avoided);
997        self.average_tokens_avoided = saturating_i128_to_isize(
998            totals
999                .measured_tokens_saved
1000                .saturating_add(totals.average_modeled_tokens_avoided),
1001        );
1002        self.maximum_tokens_avoided = saturating_i128_to_isize(
1003            totals
1004                .measured_tokens_saved
1005                .saturating_add(totals.deduped_modeled_tokens_avoided),
1006        );
1007        self.tokens_avoided = self.average_tokens_avoided;
1008        self.repeated_baselines_deduped =
1009            saturating_u128_to_usize(totals.repeated_baselines_deduped);
1010        self.observed_file_read_replacements =
1011            saturating_u128_to_usize(totals.observed_file_read_replacements);
1012        self.modeled_file_reads_avoided =
1013            saturating_u128_to_usize(totals.modeled_file_reads_avoided);
1014        self.likely_file_reads_avoided = self
1015            .observed_file_read_replacements
1016            .saturating_add(self.modeled_file_reads_avoided);
1017        self.read_avoidance_confidence = read_avoidance_confidence_for(
1018            self.observed_file_read_replacements,
1019            self.modeled_file_reads_avoided,
1020        )
1021        .to_string();
1022    }
1023
1024    /// Set the truth state for caller-label and retained raw detail.
1025    pub const fn set_detail_availability(&mut self, availability: UsageDetailAvailability) {
1026        self.detail_availability = availability;
1027    }
1028
1029    /// Apply separated measured/modeled accounting totals from raw usage events.
1030    pub fn apply_accounting_from_events(&mut self, events: &[UsageEvent]) {
1031        let summary = TokenAccountingSummary::from_events(events);
1032        self.measured_tokens_saved = summary.measured_tokens_saved;
1033        self.gross_modeled_tokens_avoided = summary.gross_modeled_tokens_avoided;
1034        self.deduped_modeled_tokens_avoided = summary.deduped_modeled_tokens_avoided;
1035        self.average_modeled_tokens_avoided = summary.average_modeled_tokens_avoided;
1036        self.average_tokens_avoided = summary.average_tokens_avoided;
1037        self.maximum_tokens_avoided = summary.maximum_tokens_avoided;
1038        self.tokens_avoided = summary.average_tokens_avoided;
1039        self.repeated_baselines_deduped = summary.repeated_baselines_deduped;
1040        self.observed_file_read_replacements = summary.observed_file_read_replacements;
1041        self.modeled_file_reads_avoided = summary.modeled_file_reads_avoided;
1042        self.likely_file_reads_avoided = summary.likely_file_reads_avoided;
1043        self.read_avoidance_confidence = read_avoidance_confidence_for(
1044            self.observed_file_read_replacements,
1045            self.modeled_file_reads_avoided,
1046        )
1047        .to_string();
1048    }
1049}
1050
1051impl TokenBucketOverview {
1052    /// Build a bucket overview from aggregate heuristic token totals.
1053    #[must_use]
1054    #[allow(clippy::too_many_arguments)]
1055    pub fn from_totals(
1056        token_savings_bucket: String,
1057        provider: String,
1058        model: String,
1059        tokenizer_backend: String,
1060        accuracy: String,
1061        baseline_kind: String,
1062        confidence: String,
1063        accounting_layer: String,
1064        estimate_method: String,
1065        denominator_kind: String,
1066        dedupe_scope: String,
1067        calls: u128,
1068        without: u128,
1069        with: u128,
1070    ) -> Self {
1071        let estimated_saved = aggregate_token_delta(without, with);
1072        let savings_rate = if without == 0 {
1073            None
1074        } else {
1075            Some((without as f64 - with as f64) / without as f64)
1076        };
1077        Self {
1078            token_savings_bucket,
1079            provider,
1080            model,
1081            tokenizer_backend,
1082            accuracy,
1083            baseline_kind,
1084            confidence,
1085            calls: saturating_u128_to_usize(calls),
1086            estimated_without_projectatlas: saturating_u128_to_usize(without),
1087            estimated_with_projectatlas: saturating_u128_to_usize(with),
1088            estimated_saved,
1089            savings_rate,
1090            accounting_layer,
1091            estimate_method,
1092            denominator_kind,
1093            dedupe_scope,
1094        }
1095    }
1096}
1097
1098impl TokenTrendPeriod {
1099    /// Build a period aggregate from token totals.
1100    #[must_use]
1101    pub fn from_totals(period: String, calls: u128, without: u128, with: u128) -> Self {
1102        let bucket = TokenBucketOverview::from_totals(
1103            default_token_savings_bucket(),
1104            default_token_provider(),
1105            default_token_model(),
1106            default_tokenizer_backend(),
1107            default_token_accuracy(),
1108            default_token_baseline_kind(),
1109            default_token_confidence(),
1110            default_accounting_layer(),
1111            default_estimate_method(),
1112            default_denominator_kind(),
1113            default_dedupe_scope(),
1114            calls,
1115            without,
1116            with,
1117        );
1118        Self::from_buckets(period, vec![bucket])
1119    }
1120
1121    /// Build a period aggregate from pre-aggregated buckets.
1122    #[must_use]
1123    pub fn from_buckets(period: String, buckets: Vec<TokenBucketOverview>) -> Self {
1124        let calls = buckets.iter().fold(0u128, |acc, bucket| {
1125            acc.saturating_add(bucket.calls as u128)
1126        });
1127        let without = buckets.iter().fold(0u128, |acc, bucket| {
1128            acc.saturating_add(bucket.estimated_without_projectatlas as u128)
1129        });
1130        let with = buckets.iter().fold(0u128, |acc, bucket| {
1131            acc.saturating_add(bucket.estimated_with_projectatlas as u128)
1132        });
1133        let saved = aggregate_token_delta(without, with);
1134        let savings_rate = if without == 0 {
1135            None
1136        } else {
1137            Some((without as f64 - with as f64) / without as f64)
1138        };
1139        Self {
1140            period,
1141            calls: saturating_u128_to_usize(calls),
1142            estimated_without_projectatlas: saturating_u128_to_usize(without),
1143            estimated_with_projectatlas: saturating_u128_to_usize(with),
1144            estimated_saved: saved,
1145            savings_rate,
1146            buckets,
1147        }
1148    }
1149}
1150
1151impl TokenTrendReport {
1152    /// Build a trend report from period aggregates.
1153    #[must_use]
1154    pub fn new(
1155        session: Option<String>,
1156        window: TokenTrendWindow,
1157        periods: Vec<TokenTrendPeriod>,
1158    ) -> Self {
1159        Self {
1160            estimate_kind: TOKEN_ESTIMATE_KIND.to_string(),
1161            estimator: TOKEN_ESTIMATOR.to_string(),
1162            estimate_scope: TOKEN_ESTIMATE_SCOPE.to_string(),
1163            session,
1164            window,
1165            periods,
1166            detail_availability: UsageDetailAvailability::Retained,
1167        }
1168    }
1169
1170    /// Set the truth state for the requested retained trend scope.
1171    pub const fn set_detail_availability(&mut self, availability: UsageDetailAvailability) {
1172        self.detail_availability = availability;
1173    }
1174}
1175
1176/// Grouping key for token bucket aggregation.
1177#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
1178struct TokenBucketKey {
1179    /// Savings bucket.
1180    token_savings_bucket: String,
1181    /// Provider used for token counting.
1182    provider: String,
1183    /// Model used for token counting.
1184    model: String,
1185    /// Tokenizer or API backend used for token counting.
1186    tokenizer_backend: String,
1187    /// Accuracy level for the token count.
1188    accuracy: String,
1189    /// Baseline scenario used for the without-ProjectAtlas estimate.
1190    baseline_kind: String,
1191    /// Confidence level for the baseline scenario.
1192    confidence: String,
1193    /// Accounting layer used to separate measured deltas from modeled avoidance.
1194    accounting_layer: String,
1195    /// Token estimate method used for this bucket.
1196    estimate_method: String,
1197    /// Denominator represented by the baseline estimate.
1198    denominator_kind: String,
1199    /// Dedupe scope used by events in this bucket.
1200    dedupe_scope: String,
1201}
1202
1203/// Stable key used to dedupe repeated modeled baselines within a session.
1204#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
1205struct ModeledBaselineKey {
1206    /// Session that emitted the modeled events.
1207    session_id: String,
1208    /// Human-readable baseline identity.
1209    baseline_identity: String,
1210    /// Stable fingerprint for the modeled baseline.
1211    baseline_fingerprint: String,
1212    /// Denominator kind represented by the baseline.
1213    denominator_kind: String,
1214}
1215
1216/// Accumulators for one modeled baseline dedupe group.
1217#[derive(Default)]
1218struct ModeledBaselineTotals {
1219    /// Number of modeled events in the group.
1220    calls: usize,
1221    /// Single baseline token count retained for the group.
1222    baseline_without_projectatlas: usize,
1223    /// Sum of all `ProjectAtlas` payload tokens emitted for the group.
1224    emitted_with_projectatlas: u128,
1225}
1226
1227/// Final separated accounting totals derived from raw usage events.
1228#[derive(Default)]
1229struct TokenAccountingSummary {
1230    /// Observed before/after saved tokens.
1231    measured_tokens_saved: isize,
1232    /// Gross modeled avoided tokens before dedupe.
1233    gross_modeled_tokens_avoided: isize,
1234    /// Modeled avoided tokens after repeated baseline dedupe.
1235    deduped_modeled_tokens_avoided: isize,
1236    /// Average-policy modeled avoided tokens after baseline dedupe.
1237    average_modeled_tokens_avoided: isize,
1238    /// Average-policy tokens avoided.
1239    average_tokens_avoided: isize,
1240    /// All-files maximum tokens avoided.
1241    maximum_tokens_avoided: isize,
1242    /// Number of duplicate modeled baseline events collapsed by dedupe.
1243    repeated_baselines_deduped: usize,
1244    /// Observed `ProjectAtlas` calls compared with full-file reads.
1245    observed_file_read_replacements: usize,
1246    /// Modeled `ProjectAtlas` calls that likely avoided whole-file reads.
1247    modeled_file_reads_avoided: usize,
1248    /// Total likely whole-file reads avoided.
1249    likely_file_reads_avoided: usize,
1250}
1251
1252impl TokenAccountingSummary {
1253    /// Build separated accounting totals from raw usage events.
1254    fn from_events(events: &[UsageEvent]) -> Self {
1255        let mut measured_tokens_saved = 0i128;
1256        let mut gross_modeled_tokens_avoided = 0i128;
1257        let mut event_scoped_modeled_tokens_avoided = 0i128;
1258        let mut average_non_directory_tokens_avoided = 0i128;
1259        let mut average_directory_without = 0u128;
1260        let mut average_directory_with = 0u128;
1261        let mut observed_file_read_replacements = 0usize;
1262        let mut modeled_file_reads_avoided = 0usize;
1263        let mut modeled_baselines = BTreeMap::<ModeledBaselineKey, ModeledBaselineTotals>::new();
1264
1265        for event in events {
1266            let (Some(without), Some(with)) = (
1267                event.estimated_tokens_without_projectatlas,
1268                event.estimated_tokens_with_projectatlas,
1269            ) else {
1270                continue;
1271            };
1272            let delta = aggregate_token_delta_wide(without as u128, with as u128);
1273            if is_observed_event(event) {
1274                measured_tokens_saved = measured_tokens_saved.saturating_add(delta);
1275                if is_observed_read_replacement_event(event, without) {
1276                    observed_file_read_replacements =
1277                        observed_file_read_replacements.saturating_add(1);
1278                }
1279                continue;
1280            }
1281            if !is_modeled_event(event) {
1282                continue;
1283            }
1284            if is_modeled_read_avoidance_event(event, without) {
1285                modeled_file_reads_avoided = modeled_file_reads_avoided.saturating_add(1);
1286            }
1287            gross_modeled_tokens_avoided = gross_modeled_tokens_avoided.saturating_add(delta);
1288            if event.dedupe_scope == TOKEN_DEDUPE_SCOPE_EVENT {
1289                event_scoped_modeled_tokens_avoided =
1290                    event_scoped_modeled_tokens_avoided.saturating_add(delta);
1291                if event.denominator_kind == TOKEN_BASELINE_DIRECTORY_WALK {
1292                    average_directory_without =
1293                        average_directory_without.saturating_add(without as u128);
1294                    average_directory_with = average_directory_with.saturating_add(with as u128);
1295                } else {
1296                    average_non_directory_tokens_avoided =
1297                        average_non_directory_tokens_avoided.saturating_add(delta);
1298                }
1299                continue;
1300            }
1301            let entry = modeled_baselines
1302                .entry(ModeledBaselineKey::from_event(event))
1303                .or_default();
1304            entry.calls = entry.calls.saturating_add(1);
1305            entry.baseline_without_projectatlas = entry.baseline_without_projectatlas.max(without);
1306            entry.emitted_with_projectatlas =
1307                entry.emitted_with_projectatlas.saturating_add(with as u128);
1308        }
1309
1310        let mut deduped_modeled_tokens_avoided = event_scoped_modeled_tokens_avoided;
1311        let mut repeated_baselines_deduped = 0usize;
1312        for (key, totals) in &modeled_baselines {
1313            if totals.calls > 1 {
1314                repeated_baselines_deduped =
1315                    repeated_baselines_deduped.saturating_add(totals.calls.saturating_sub(1));
1316            }
1317            let delta = aggregate_token_delta_wide(
1318                totals.baseline_without_projectatlas as u128,
1319                totals.emitted_with_projectatlas,
1320            );
1321            deduped_modeled_tokens_avoided = deduped_modeled_tokens_avoided.saturating_add(delta);
1322            if key.denominator_kind == TOKEN_BASELINE_DIRECTORY_WALK {
1323                average_directory_without = average_directory_without
1324                    .saturating_add(totals.baseline_without_projectatlas as u128);
1325                average_directory_with =
1326                    average_directory_with.saturating_add(totals.emitted_with_projectatlas);
1327            } else {
1328                average_non_directory_tokens_avoided =
1329                    average_non_directory_tokens_avoided.saturating_add(delta);
1330            }
1331        }
1332        let average_directory_tokens_avoided = aggregate_token_delta_wide(
1333            average_modeled_baseline_tokens(
1334                TOKEN_BASELINE_DIRECTORY_WALK,
1335                average_directory_without,
1336            ),
1337            average_directory_with,
1338        );
1339        let average_modeled_tokens_avoided =
1340            average_non_directory_tokens_avoided.saturating_add(average_directory_tokens_avoided);
1341        let average_tokens_avoided =
1342            measured_tokens_saved.saturating_add(average_modeled_tokens_avoided);
1343        let maximum_tokens_avoided =
1344            measured_tokens_saved.saturating_add(deduped_modeled_tokens_avoided);
1345        let likely_file_reads_avoided =
1346            observed_file_read_replacements.saturating_add(modeled_file_reads_avoided);
1347        Self {
1348            measured_tokens_saved: saturating_i128_to_isize(measured_tokens_saved),
1349            gross_modeled_tokens_avoided: saturating_i128_to_isize(gross_modeled_tokens_avoided),
1350            deduped_modeled_tokens_avoided: saturating_i128_to_isize(
1351                deduped_modeled_tokens_avoided,
1352            ),
1353            average_modeled_tokens_avoided: saturating_i128_to_isize(
1354                average_modeled_tokens_avoided,
1355            ),
1356            average_tokens_avoided: saturating_i128_to_isize(average_tokens_avoided),
1357            maximum_tokens_avoided: saturating_i128_to_isize(maximum_tokens_avoided),
1358            repeated_baselines_deduped,
1359            observed_file_read_replacements,
1360            modeled_file_reads_avoided,
1361            likely_file_reads_avoided,
1362        }
1363    }
1364}
1365
1366impl ModeledBaselineKey {
1367    /// Build a dedupe key from persisted event metadata with legacy fallback.
1368    fn from_event(event: &UsageEvent) -> Self {
1369        let identity = if event.baseline_identity.is_empty() {
1370            default_baseline_identity(
1371                &event.command,
1372                event.path.as_deref(),
1373                event.query.as_deref(),
1374                &event.baseline_kind,
1375            )
1376        } else {
1377            event.baseline_identity.clone()
1378        };
1379        let fingerprint = if event.baseline_fingerprint.is_empty() {
1380            identity.clone()
1381        } else {
1382            event.baseline_fingerprint.clone()
1383        };
1384        Self {
1385            session_id: event.session_id.clone(),
1386            baseline_identity: identity,
1387            baseline_fingerprint: fingerprint,
1388            denominator_kind: event.denominator_kind.clone(),
1389        }
1390    }
1391}
1392
1393impl TokenBucketKey {
1394    /// Build a grouping key from one usage event.
1395    fn from(event: &UsageEvent) -> Self {
1396        let observed = is_observed_event(event);
1397        Self {
1398            token_savings_bucket: event.token_savings_bucket.clone(),
1399            provider: event.provider.clone(),
1400            model: event.model.clone(),
1401            tokenizer_backend: event.tokenizer_backend.clone(),
1402            accuracy: event.accuracy.clone(),
1403            baseline_kind: event.baseline_kind.clone(),
1404            confidence: event.confidence.clone(),
1405            accounting_layer: if observed {
1406                TOKEN_ACCOUNTING_OBSERVED_DELTA.to_string()
1407            } else {
1408                event.accounting_layer.clone()
1409            },
1410            estimate_method: event.estimate_method.clone(),
1411            denominator_kind: if observed {
1412                TOKEN_BASELINE_FULL_FILE.to_string()
1413            } else {
1414                event.denominator_kind.clone()
1415            },
1416            dedupe_scope: if observed {
1417                TOKEN_DEDUPE_SCOPE_EVENT.to_string()
1418            } else {
1419                event.dedupe_scope.clone()
1420            },
1421        }
1422    }
1423
1424    /// Convert an aggregate bucket into a report row.
1425    fn into_overview(self, calls: u128, without: u128, with: u128) -> TokenBucketOverview {
1426        TokenBucketOverview::from_totals(
1427            self.token_savings_bucket,
1428            self.provider,
1429            self.model,
1430            self.tokenizer_backend,
1431            self.accuracy,
1432            self.baseline_kind,
1433            self.confidence,
1434            self.accounting_layer,
1435            self.estimate_method,
1436            self.denominator_kind,
1437            self.dedupe_scope,
1438            calls,
1439            without,
1440            with,
1441        )
1442    }
1443}
1444
1445/// Create a usage event from response text and baseline text.
1446#[must_use]
1447pub fn usage_from_text(
1448    session_id: &str,
1449    command: &str,
1450    path: Option<String>,
1451    query: Option<String>,
1452    baseline_text: &str,
1453    projectatlas_text: &str,
1454) -> UsageEvent {
1455    let without = estimate_tokens(baseline_text);
1456    let with = estimate_tokens(projectatlas_text);
1457    usage_from_estimates_with_accounting(
1458        session_id,
1459        command,
1460        path,
1461        query,
1462        without,
1463        with,
1464        TOKEN_BUCKET_FULL_FILE_COMPRESSION,
1465        TOKEN_BASELINE_FULL_FILE,
1466        TOKEN_CONFIDENCE_OBSERVED,
1467        TOKEN_ACCOUNTING_OBSERVED_DELTA,
1468        TOKEN_BASELINE_FULL_FILE,
1469        TOKEN_DEDUPE_SCOPE_EVENT,
1470    )
1471}
1472
1473/// Create a usage event from already-computed token estimates.
1474#[must_use]
1475pub fn usage_from_estimates(
1476    session_id: &str,
1477    command: &str,
1478    path: Option<String>,
1479    query: Option<String>,
1480    estimated_without_projectatlas: usize,
1481    estimated_with_projectatlas: usize,
1482) -> UsageEvent {
1483    usage_from_estimates_with_accounting(
1484        session_id,
1485        command,
1486        path,
1487        query,
1488        estimated_without_projectatlas,
1489        estimated_with_projectatlas,
1490        TOKEN_BUCKET_NAVIGATION_AVOIDANCE,
1491        TOKEN_BASELINE_SELECTED_CANDIDATES,
1492        TOKEN_CONFIDENCE_INFERRED,
1493        TOKEN_ACCOUNTING_MODELED_AVOIDANCE,
1494        TOKEN_BASELINE_SELECTED_CANDIDATES,
1495        TOKEN_DEDUPE_SCOPE_SESSION,
1496    )
1497}
1498
1499/// Create a usage event from token estimates and explicit baseline semantics.
1500#[must_use]
1501#[allow(clippy::too_many_arguments)]
1502pub fn usage_from_estimates_with_context(
1503    session_id: &str,
1504    command: &str,
1505    path: Option<String>,
1506    query: Option<String>,
1507    estimated_without_projectatlas: usize,
1508    estimated_with_projectatlas: usize,
1509    token_savings_bucket: &str,
1510    baseline_kind: &str,
1511    confidence: &str,
1512) -> UsageEvent {
1513    usage_from_estimates_with_accounting(
1514        session_id,
1515        command,
1516        path,
1517        query,
1518        estimated_without_projectatlas,
1519        estimated_with_projectatlas,
1520        token_savings_bucket,
1521        baseline_kind,
1522        confidence,
1523        if token_savings_bucket == TOKEN_BUCKET_FULL_FILE_COMPRESSION {
1524            TOKEN_ACCOUNTING_OBSERVED_DELTA
1525        } else {
1526            TOKEN_ACCOUNTING_MODELED_AVOIDANCE
1527        },
1528        baseline_kind,
1529        if token_savings_bucket == TOKEN_BUCKET_FULL_FILE_COMPRESSION {
1530            TOKEN_DEDUPE_SCOPE_EVENT
1531        } else {
1532            TOKEN_DEDUPE_SCOPE_SESSION
1533        },
1534    )
1535}
1536
1537/// Create a usage event from token estimates and explicit accounting semantics.
1538#[must_use]
1539#[allow(clippy::too_many_arguments)]
1540pub fn usage_from_estimates_with_accounting(
1541    session_id: &str,
1542    command: &str,
1543    path: Option<String>,
1544    query: Option<String>,
1545    estimated_without_projectatlas: usize,
1546    estimated_with_projectatlas: usize,
1547    token_savings_bucket: &str,
1548    baseline_kind: &str,
1549    confidence: &str,
1550    accounting_layer: &str,
1551    denominator_kind: &str,
1552    dedupe_scope: &str,
1553) -> UsageEvent {
1554    let baseline_identity =
1555        default_baseline_identity(command, path.as_deref(), query.as_deref(), baseline_kind);
1556    let baseline_fingerprint = baseline_identity.clone();
1557    UsageEvent {
1558        session_id: session_id.to_string(),
1559        command: command.to_string(),
1560        path,
1561        query,
1562        estimated_tokens_without_projectatlas: Some(estimated_without_projectatlas),
1563        estimated_tokens_with_projectatlas: Some(estimated_with_projectatlas),
1564        estimated_tokens_saved: Some(token_delta(
1565            estimated_without_projectatlas,
1566            estimated_with_projectatlas,
1567        )),
1568        token_savings_bucket: token_savings_bucket.to_string(),
1569        provider: default_token_provider(),
1570        model: default_token_model(),
1571        tokenizer_backend: default_tokenizer_backend(),
1572        accuracy: default_token_accuracy(),
1573        baseline_kind: baseline_kind.to_string(),
1574        confidence: confidence.to_string(),
1575        calculation_trace: default_token_trace(),
1576        accounting_layer: accounting_layer.to_string(),
1577        estimate_method: default_estimate_method(),
1578        denominator_kind: denominator_kind.to_string(),
1579        baseline_identity,
1580        baseline_fingerprint,
1581        dedupe_scope: dedupe_scope.to_string(),
1582    }
1583}
1584
1585/// Default token savings bucket for legacy usage events.
1586#[must_use]
1587pub fn default_token_savings_bucket() -> String {
1588    TOKEN_BUCKET_NAVIGATION_AVOIDANCE.to_string()
1589}
1590
1591/// Default token provider for legacy usage events.
1592#[must_use]
1593pub fn default_token_provider() -> String {
1594    TOKEN_PROVIDER_HEURISTIC.to_string()
1595}
1596
1597/// Default token model for legacy usage events.
1598#[must_use]
1599pub fn default_token_model() -> String {
1600    TOKEN_MODEL_UNKNOWN.to_string()
1601}
1602
1603/// Default tokenizer backend for legacy usage events.
1604#[must_use]
1605pub fn default_tokenizer_backend() -> String {
1606    TOKENIZER_BACKEND_HEURISTIC.to_string()
1607}
1608
1609/// Default accuracy label for legacy usage events.
1610#[must_use]
1611pub fn default_token_accuracy() -> String {
1612    TOKEN_ACCURACY_HEURISTIC.to_string()
1613}
1614
1615/// Default baseline kind for legacy usage events.
1616#[must_use]
1617pub fn default_token_baseline_kind() -> String {
1618    TOKEN_BASELINE_SELECTED_CANDIDATES.to_string()
1619}
1620
1621/// Default confidence label for legacy usage events.
1622#[must_use]
1623pub fn default_token_confidence() -> String {
1624    TOKEN_CONFIDENCE_INFERRED.to_string()
1625}
1626
1627/// Default calculation trace for legacy usage events.
1628#[must_use]
1629pub fn default_token_trace() -> String {
1630    TOKEN_TRACE_HEURISTIC.to_string()
1631}
1632
1633/// Default accounting layer for legacy usage events.
1634#[must_use]
1635pub fn default_accounting_layer() -> String {
1636    TOKEN_ACCOUNTING_MODELED_AVOIDANCE.to_string()
1637}
1638
1639/// Default estimate method for legacy usage events.
1640#[must_use]
1641pub fn default_estimate_method() -> String {
1642    TOKEN_ESTIMATE_METHOD_HEURISTIC.to_string()
1643}
1644
1645/// Default denominator kind for legacy usage events.
1646#[must_use]
1647pub fn default_denominator_kind() -> String {
1648    TOKEN_BASELINE_SELECTED_CANDIDATES.to_string()
1649}
1650
1651/// Default dedupe scope for legacy usage events.
1652#[must_use]
1653pub fn default_dedupe_scope() -> String {
1654    TOKEN_DEDUPE_SCOPE_SESSION.to_string()
1655}
1656
1657/// Default read-avoidance scope for legacy serialized overviews.
1658#[must_use]
1659pub fn default_read_avoidance_scope() -> String {
1660    READ_AVOIDANCE_SCOPE.to_string()
1661}
1662
1663/// Default read-avoidance confidence for legacy serialized overviews.
1664#[must_use]
1665pub fn default_read_avoidance_confidence() -> String {
1666    READ_AVOIDANCE_CONFIDENCE_NOT_RECORDED.to_string()
1667}
1668
1669/// Build a stable baseline identity from existing event context.
1670#[must_use]
1671pub fn default_baseline_identity(
1672    command: &str,
1673    path: Option<&str>,
1674    query: Option<&str>,
1675    baseline_kind: &str,
1676) -> String {
1677    format!(
1678        "{baseline_kind}:command={command}:path={path}:query={query}",
1679        path = path.unwrap_or("*"),
1680        query = query.unwrap_or("*")
1681    )
1682}
1683
1684/// Return a saturating signed token delta.
1685fn token_delta(without: usize, with: usize) -> isize {
1686    let without = isize::try_from(without).unwrap_or(isize::MAX);
1687    let with = isize::try_from(with).unwrap_or(isize::MAX);
1688    without.saturating_sub(with)
1689}
1690
1691/// Return the signed aggregate token delta.
1692fn aggregate_token_delta(without: u128, with: u128) -> isize {
1693    saturating_i128_to_isize(aggregate_token_delta_wide(without, with))
1694}
1695
1696/// Return a wide signed aggregate token delta and saturate only at the wide boundary.
1697fn aggregate_token_delta_wide(without: u128, with: u128) -> i128 {
1698    if without >= with {
1699        let delta = without - with;
1700        if delta > i128::MAX as u128 {
1701            i128::MAX
1702        } else {
1703            delta as i128
1704        }
1705    } else {
1706        let delta = with - without;
1707        if delta > i128::MAX as u128 {
1708            i128::MIN
1709        } else {
1710            -(delta as i128)
1711        }
1712    }
1713}
1714
1715/// Apply the fixed average policy to one modeled baseline.
1716#[must_use]
1717pub fn average_modeled_baseline_tokens(denominator_kind: &str, without: u128) -> u128 {
1718    if denominator_kind == TOKEN_BASELINE_DIRECTORY_WALK {
1719        without / 2
1720    } else {
1721        without
1722    }
1723}
1724
1725/// Convert a wide aggregate count to `usize` with saturation.
1726fn saturating_u128_to_usize(value: u128) -> usize {
1727    if value > usize::MAX as u128 {
1728        usize::MAX
1729    } else {
1730        value as usize
1731    }
1732}
1733
1734/// Convert a wide signed aggregate to `isize` with saturation.
1735fn saturating_i128_to_isize(value: i128) -> isize {
1736    if value > isize::MAX as i128 {
1737        isize::MAX
1738    } else if value < isize::MIN as i128 {
1739        isize::MIN
1740    } else {
1741        value as isize
1742    }
1743}
1744
1745/// Sum observed saved-token buckets.
1746fn measured_tokens_saved_from_buckets(buckets: &[TokenBucketOverview]) -> i128 {
1747    buckets
1748        .iter()
1749        .filter(|bucket| is_observed_bucket(bucket))
1750        .fold(0i128, |acc, bucket| {
1751            acc.saturating_add(bucket.estimated_saved as i128)
1752        })
1753}
1754
1755/// Sum modeled avoided-token buckets.
1756fn modeled_tokens_saved_from_buckets(buckets: &[TokenBucketOverview]) -> i128 {
1757    buckets
1758        .iter()
1759        .filter(|bucket| is_modeled_bucket(bucket))
1760        .fold(0i128, |acc, bucket| {
1761            acc.saturating_add(bucket.estimated_saved as i128)
1762        })
1763}
1764
1765/// Sum modeled avoided-token buckets with the average directory-walk policy.
1766fn average_modeled_tokens_saved_from_buckets(buckets: &[TokenBucketOverview]) -> i128 {
1767    let mut non_directory_tokens_avoided = 0i128;
1768    let mut directory_without = 0u128;
1769    let mut directory_with = 0u128;
1770    for bucket in buckets.iter().filter(|bucket| is_modeled_bucket(bucket)) {
1771        if bucket.denominator_kind == TOKEN_BASELINE_DIRECTORY_WALK {
1772            directory_without =
1773                directory_without.saturating_add(bucket.estimated_without_projectatlas as u128);
1774            directory_with =
1775                directory_with.saturating_add(bucket.estimated_with_projectatlas as u128);
1776        } else {
1777            non_directory_tokens_avoided =
1778                non_directory_tokens_avoided.saturating_add(bucket.estimated_saved as i128);
1779        }
1780    }
1781    let directory_tokens_avoided = aggregate_token_delta_wide(
1782        average_modeled_baseline_tokens(TOKEN_BASELINE_DIRECTORY_WALK, directory_without),
1783        directory_with,
1784    );
1785    non_directory_tokens_avoided.saturating_add(directory_tokens_avoided)
1786}
1787
1788/// Whether an event represents observed before/after source compression.
1789fn is_observed_event(event: &UsageEvent) -> bool {
1790    event.accounting_layer == TOKEN_ACCOUNTING_OBSERVED_DELTA
1791        || event.token_savings_bucket == TOKEN_BUCKET_FULL_FILE_COMPRESSION
1792        || event.confidence == TOKEN_CONFIDENCE_OBSERVED
1793}
1794
1795/// Whether an event represents modeled counterfactual navigation avoidance.
1796fn is_modeled_event(event: &UsageEvent) -> bool {
1797    event.accounting_layer == TOKEN_ACCOUNTING_MODELED_AVOIDANCE || !is_observed_event(event)
1798}
1799
1800/// Whether a raw observed event is strong evidence for replacing a whole-file read.
1801fn is_observed_read_replacement_event(event: &UsageEvent, baseline_tokens: usize) -> bool {
1802    baseline_tokens > 0
1803        && matches!(
1804            event.command.as_str(),
1805            TOKEN_COMMAND_SUMMARY
1806                | TOKEN_COMMAND_OUTLINE
1807                | TOKEN_COMMAND_SLICE
1808                | TOKEN_COMMAND_SYMBOL_SLICE
1809                | TOKEN_COMMAND_MCP_FILE_SUMMARY
1810                | TOKEN_COMMAND_MCP_OUTLINE
1811                | TOKEN_COMMAND_MCP_SLICE
1812        )
1813}
1814
1815/// Whether a raw modeled event is strong evidence for avoiding a broad file read.
1816fn is_modeled_read_avoidance_event(event: &UsageEvent, baseline_tokens: usize) -> bool {
1817    baseline_tokens > 0
1818        && matches!(
1819            event.command.as_str(),
1820            TOKEN_COMMAND_SEARCH | TOKEN_COMMAND_MCP_SEARCH
1821        )
1822        && event.denominator_kind == TOKEN_BASELINE_SELECTED_CANDIDATES
1823}
1824
1825/// Return the confidence label for read-avoidance counters.
1826fn read_avoidance_confidence_for(
1827    observed_file_read_replacements: usize,
1828    modeled_file_reads_avoided: usize,
1829) -> &'static str {
1830    if observed_file_read_replacements == 0 && modeled_file_reads_avoided == 0 {
1831        READ_AVOIDANCE_CONFIDENCE_NOT_RECORDED
1832    } else if modeled_file_reads_avoided == 0 {
1833        READ_AVOIDANCE_CONFIDENCE_OBSERVED
1834    } else {
1835        READ_AVOIDANCE_CONFIDENCE_MODELED
1836    }
1837}
1838
1839/// Whether a bucket represents observed before/after source compression.
1840fn is_observed_bucket(bucket: &TokenBucketOverview) -> bool {
1841    bucket.accounting_layer == TOKEN_ACCOUNTING_OBSERVED_DELTA
1842        || bucket.token_savings_bucket == TOKEN_BUCKET_FULL_FILE_COMPRESSION
1843        || bucket.confidence == TOKEN_CONFIDENCE_OBSERVED
1844}
1845
1846/// Whether a bucket represents modeled counterfactual navigation avoidance.
1847fn is_modeled_bucket(bucket: &TokenBucketOverview) -> bool {
1848    bucket.accounting_layer == TOKEN_ACCOUNTING_MODELED_AVOIDANCE || !is_observed_bucket(bucket)
1849}
1850
1851#[cfg(test)]
1852mod tests {
1853    use super::{
1854        AgentEfficiencyEvidenceState, READ_AVOIDANCE_CONFIDENCE_MODELED,
1855        READ_AVOIDANCE_CONFIDENCE_NOT_RECORDED, READ_AVOIDANCE_CONFIDENCE_OBSERVED,
1856        READ_AVOIDANCE_SCOPE, TOKEN_AVERAGE_DIRECTORY_WALK_PERCENT, TOKEN_BASELINE_DIRECTORY_WALK,
1857        TOKEN_BUCKET_FULL_FILE_COMPRESSION, TOKEN_BUCKET_NAVIGATION_AVOIDANCE,
1858        TOKEN_DEDUPE_SCOPE_EVENT, TOKEN_ESTIMATE_KIND, TOKEN_ESTIMATE_SCOPE, TOKEN_ESTIMATOR,
1859        TelemetryContractError, TokenAccountingTotals, TokenOverview, TokenTrendReport,
1860        TokenTrendWindow, UsageDetailAvailability, UsageInstanceId, UsageInstanceOwner,
1861        usage_from_estimates, usage_from_text,
1862    };
1863    use std::io;
1864
1865    fn require_eq<T: std::fmt::Debug + PartialEq>(
1866        actual: &T,
1867        expected: &T,
1868        label: &str,
1869    ) -> Result<(), Box<dyn std::error::Error>> {
1870        if actual == expected {
1871            Ok(())
1872        } else {
1873            Err(io::Error::other(format!(
1874                "{label} mismatch: expected {expected:?}, got {actual:?}"
1875            ))
1876            .into())
1877        }
1878    }
1879
1880    #[test]
1881    fn usage_instance_ids_validate_and_round_trip() {
1882        let bytes = [7; 16];
1883        let identity = UsageInstanceId::from_bytes(bytes);
1884        assert_eq!(identity.map(UsageInstanceId::as_bytes), Ok(bytes));
1885        assert_eq!(
1886            UsageInstanceId::from_bytes([0; 16]),
1887            Err(TelemetryContractError::ZeroUsageInstanceId)
1888        );
1889    }
1890
1891    #[test]
1892    fn usage_states_parse_and_missing_report_state_fails_honest()
1893    -> Result<(), Box<dyn std::error::Error>> {
1894        for (value, expected) in [
1895            ("cli_invocation", UsageInstanceOwner::CliInvocation),
1896            ("mcp_process", UsageInstanceOwner::McpProcess),
1897            ("library_handle", UsageInstanceOwner::LibraryHandle),
1898            ("migrated_legacy", UsageInstanceOwner::MigratedLegacy),
1899        ] {
1900            require_eq(
1901                &UsageInstanceOwner::parse(value),
1902                &Some(expected),
1903                "usage instance owner parse",
1904            )?;
1905            require_eq(&expected.as_str(), &value, "usage instance owner encoding")?;
1906        }
1907        require_eq(
1908            &UsageInstanceOwner::parse("unknown"),
1909            &None,
1910            "unknown usage instance owner",
1911        )?;
1912
1913        for (value, expected) in [
1914            ("retained", UsageDetailAvailability::Retained),
1915            ("partial", UsageDetailAvailability::Partial),
1916            ("expired", UsageDetailAvailability::Expired),
1917            ("unavailable", UsageDetailAvailability::Unavailable),
1918        ] {
1919            require_eq(
1920                &UsageDetailAvailability::parse(value),
1921                &Some(expected),
1922                "detail availability parse",
1923            )?;
1924            require_eq(&expected.as_str(), &value, "detail availability encoding")?;
1925        }
1926        require_eq(
1927            &UsageDetailAvailability::parse("unknown"),
1928            &None,
1929            "unknown detail availability",
1930        )?;
1931        require_eq(
1932            &UsageDetailAvailability::default(),
1933            &UsageDetailAvailability::Unavailable,
1934            "default detail availability",
1935        )?;
1936
1937        let overview = TokenOverview::from_events(&[]);
1938        require_eq(
1939            &overview.detail_availability,
1940            &UsageDetailAvailability::Retained,
1941            "new overview detail availability",
1942        )?;
1943        let mut overview_value = serde_json::to_value(overview)?;
1944        let overview_object = overview_value
1945            .as_object_mut()
1946            .ok_or_else(|| io::Error::other("serialized token overview was not an object"))?;
1947        overview_object.remove("detail_availability");
1948        overview_object.remove("agent_efficiency");
1949        overview_object.remove("average_modeled_tokens_avoided");
1950        overview_object.remove("average_tokens_avoided");
1951        overview_object.remove("maximum_tokens_avoided");
1952        overview_object.remove("average_policy");
1953        let decoded_overview: TokenOverview = serde_json::from_value(overview_value)?;
1954        require_eq(
1955            &decoded_overview.detail_availability,
1956            &UsageDetailAvailability::Unavailable,
1957            "missing overview detail availability",
1958        )?;
1959        require_eq(
1960            &decoded_overview.agent_efficiency.state,
1961            &AgentEfficiencyEvidenceState::Unavailable,
1962            "missing agent-efficiency evidence state",
1963        )?;
1964        require_eq(
1965            &decoded_overview.agent_efficiency.baselines,
1966            &Vec::new(),
1967            "missing agent-efficiency baseline rows",
1968        )?;
1969        require_eq(
1970            &decoded_overview.average_tokens_avoided,
1971            &0,
1972            "missing average tokens avoided",
1973        )?;
1974        require_eq(
1975            &decoded_overview.maximum_tokens_avoided,
1976            &0,
1977            "missing maximum tokens avoided",
1978        )?;
1979        require_eq(
1980            &decoded_overview
1981                .average_policy
1982                .directory_walk_baseline_percent,
1983            &TOKEN_AVERAGE_DIRECTORY_WALK_PERCENT,
1984            "missing average policy",
1985        )?;
1986        require_eq(
1987            &AgentEfficiencyEvidenceState::Partial.as_str(),
1988            &"partial",
1989            "agent-efficiency evidence encoding",
1990        )?;
1991
1992        let trends = TokenTrendReport::new(None, TokenTrendWindow::Day, Vec::new());
1993        require_eq(
1994            &trends.detail_availability,
1995            &UsageDetailAvailability::Retained,
1996            "new trend detail availability",
1997        )?;
1998        let mut trends_value = serde_json::to_value(trends)?;
1999        let trends_object = trends_value
2000            .as_object_mut()
2001            .ok_or_else(|| io::Error::other("serialized token trends were not an object"))?;
2002        trends_object.remove("detail_availability");
2003        let decoded_trends: TokenTrendReport = serde_json::from_value(trends_value)?;
2004        require_eq(
2005            &decoded_trends.detail_availability,
2006            &UsageDetailAvailability::Unavailable,
2007            "missing trend detail availability",
2008        )?;
2009        Ok(())
2010    }
2011
2012    #[test]
2013    fn modeled_baseline_keys_preserve_legacy_fallback_and_component_boundaries() {
2014        let event = usage_from_estimates(
2015            "session",
2016            "search",
2017            Some("src/lib.rs".to_string()),
2018            Some("needle".to_string()),
2019            100,
2020            20,
2021        );
2022        let expected_key = event.modeled_baseline_key();
2023        assert_eq!(
2024            event.effective_baseline_identity().as_ref(),
2025            event.baseline_identity
2026        );
2027        assert_eq!(
2028            event.effective_baseline_fingerprint().as_ref(),
2029            event.baseline_fingerprint
2030        );
2031
2032        let mut legacy = event.clone();
2033        legacy.baseline_identity.clear();
2034        legacy.baseline_fingerprint.clear();
2035        assert_eq!(legacy.modeled_baseline_key(), expected_key);
2036
2037        let mut changed_fingerprint = event.clone();
2038        changed_fingerprint
2039            .baseline_fingerprint
2040            .push_str("-changed");
2041        assert_ne!(changed_fingerprint.modeled_baseline_key(), expected_key);
2042
2043        let mut left = event.clone();
2044        left.baseline_identity = "ab".to_string();
2045        left.baseline_fingerprint = "c".to_string();
2046        let mut right = event;
2047        right.baseline_identity = "a".to_string();
2048        right.baseline_fingerprint = "bc".to_string();
2049        assert_ne!(left.modeled_baseline_key(), right.modeled_baseline_key());
2050    }
2051
2052    #[test]
2053    fn wide_accounting_totals_narrow_only_at_the_report_boundary() {
2054        let mut overview = TokenOverview::from_events(&[]);
2055        overview.apply_accounting_totals(TokenAccountingTotals {
2056            measured_tokens_saved: 7,
2057            gross_modeled_tokens_avoided: 100,
2058            deduped_modeled_tokens_avoided: 30,
2059            average_modeled_tokens_avoided: 10,
2060            repeated_baselines_deduped: 2,
2061            observed_file_read_replacements: 1,
2062            modeled_file_reads_avoided: 3,
2063        });
2064        assert_eq!(overview.measured_tokens_saved, 7);
2065        assert_eq!(overview.gross_modeled_tokens_avoided, 100);
2066        assert_eq!(overview.deduped_modeled_tokens_avoided, 30);
2067        assert_eq!(overview.average_modeled_tokens_avoided, 10);
2068        assert_eq!(overview.average_tokens_avoided, 17);
2069        assert_eq!(overview.maximum_tokens_avoided, 37);
2070        assert_eq!(overview.tokens_avoided, overview.average_tokens_avoided);
2071        assert_eq!(overview.repeated_baselines_deduped, 2);
2072        assert_eq!(overview.observed_file_read_replacements, 1);
2073        assert_eq!(overview.modeled_file_reads_avoided, 3);
2074        assert_eq!(overview.likely_file_reads_avoided, 4);
2075        assert_eq!(
2076            overview.read_avoidance_confidence,
2077            READ_AVOIDANCE_CONFIDENCE_MODELED
2078        );
2079
2080        overview.apply_accounting_totals(TokenAccountingTotals {
2081            measured_tokens_saved: i128::MAX,
2082            gross_modeled_tokens_avoided: i128::MIN,
2083            deduped_modeled_tokens_avoided: i128::MAX,
2084            average_modeled_tokens_avoided: i128::MAX,
2085            repeated_baselines_deduped: u128::MAX,
2086            observed_file_read_replacements: u128::MAX,
2087            modeled_file_reads_avoided: u128::MAX,
2088        });
2089        assert_eq!(overview.measured_tokens_saved, isize::MAX);
2090        assert_eq!(overview.gross_modeled_tokens_avoided, isize::MIN);
2091        assert_eq!(overview.deduped_modeled_tokens_avoided, isize::MAX);
2092        assert_eq!(overview.average_modeled_tokens_avoided, isize::MAX);
2093        assert_eq!(overview.average_tokens_avoided, isize::MAX);
2094        assert_eq!(overview.maximum_tokens_avoided, isize::MAX);
2095        assert_eq!(overview.tokens_avoided, isize::MAX);
2096        assert_eq!(overview.repeated_baselines_deduped, usize::MAX);
2097        assert_eq!(overview.observed_file_read_replacements, usize::MAX);
2098        assert_eq!(overview.modeled_file_reads_avoided, usize::MAX);
2099        assert_eq!(overview.likely_file_reads_avoided, usize::MAX);
2100    }
2101
2102    #[test]
2103    fn usage_from_text_tracks_positive_and_negative_savings() {
2104        let positive = usage_from_text("s", "outline", None, None, "abcdefghijkl", "abcd");
2105        assert_eq!(positive.estimated_tokens_without_projectatlas, Some(3));
2106        assert_eq!(positive.estimated_tokens_with_projectatlas, Some(1));
2107        assert_eq!(positive.estimated_tokens_saved, Some(2));
2108
2109        let negative = usage_from_estimates("s", "overview", None, None, 1, 4);
2110        assert_eq!(negative.estimated_tokens_saved, Some(-3));
2111    }
2112
2113    #[test]
2114    fn huge_estimates_use_saturating_signed_delta() {
2115        let event = usage_from_estimates("s", "large-repo", None, None, usize::MAX, 0);
2116        assert_eq!(event.estimated_tokens_saved, Some(isize::MAX));
2117    }
2118
2119    #[test]
2120    fn overview_recomputes_saved_from_aggregate_without_and_with() {
2121        let mut first = usage_from_estimates("s", "a", None, None, 20, 50);
2122        first.estimated_tokens_saved = Some(999);
2123        let mut second = usage_from_estimates("s", "b", None, None, 0, 10);
2124        second.estimated_tokens_saved = Some(999);
2125        let overview = TokenOverview::from_events(&[first, second]);
2126
2127        assert_eq!(overview.estimate_kind, TOKEN_ESTIMATE_KIND);
2128        assert_eq!(overview.estimator, TOKEN_ESTIMATOR);
2129        assert_eq!(overview.estimate_scope, TOKEN_ESTIMATE_SCOPE);
2130        assert_eq!(overview.calls, 2);
2131        assert_eq!(overview.estimated_without_projectatlas, 20);
2132        assert_eq!(overview.estimated_with_projectatlas, 60);
2133        assert_eq!(overview.estimated_saved, -40);
2134        assert_eq!(overview.savings_rate, Some(-2.0));
2135    }
2136
2137    #[test]
2138    fn overview_keeps_source_compression_and_navigation_buckets_separate() {
2139        let overview = TokenOverview::from_events(&[
2140            usage_from_text("s", "summary", None, None, "abcdefghijkl", "abcd"),
2141            usage_from_estimates("s", "search", None, None, 100, 20),
2142        ]);
2143
2144        assert_eq!(overview.calls, 2);
2145        assert_eq!(overview.buckets.len(), 2);
2146        assert_eq!(
2147            overview.buckets[0].token_savings_bucket,
2148            TOKEN_BUCKET_FULL_FILE_COMPRESSION
2149        );
2150        assert_eq!(
2151            overview.buckets[1].token_savings_bucket,
2152            TOKEN_BUCKET_NAVIGATION_AVOIDANCE
2153        );
2154        assert_eq!(overview.observed_file_read_replacements, 1);
2155        assert_eq!(overview.modeled_file_reads_avoided, 1);
2156        assert_eq!(overview.likely_file_reads_avoided, 2);
2157        assert_eq!(
2158            overview.read_avoidance_confidence,
2159            READ_AVOIDANCE_CONFIDENCE_MODELED
2160        );
2161        assert_eq!(overview.read_avoidance_scope, READ_AVOIDANCE_SCOPE);
2162    }
2163
2164    #[test]
2165    fn observed_only_overview_reports_observed_read_avoidance_confidence() {
2166        let overview = TokenOverview::from_events(&[usage_from_text(
2167            "s",
2168            "summary",
2169            None,
2170            None,
2171            "abcdefghijkl",
2172            "abcd",
2173        )]);
2174
2175        assert_eq!(overview.observed_file_read_replacements, 1);
2176        assert_eq!(overview.modeled_file_reads_avoided, 0);
2177        assert_eq!(overview.likely_file_reads_avoided, 1);
2178        assert_eq!(
2179            overview.read_avoidance_confidence,
2180            READ_AVOIDANCE_CONFIDENCE_OBSERVED
2181        );
2182    }
2183
2184    #[test]
2185    fn bucket_overview_does_not_infer_read_avoidance_without_raw_events() {
2186        let event_overview = TokenOverview::from_events(&[
2187            usage_from_text("s", "summary", None, None, "abcdefghijkl", "abcd"),
2188            usage_from_estimates("s", "search", None, None, 100, 20),
2189        ]);
2190        let bucket_overview = TokenOverview::from_buckets(event_overview.buckets);
2191
2192        assert_eq!(bucket_overview.observed_file_read_replacements, 0);
2193        assert_eq!(bucket_overview.modeled_file_reads_avoided, 0);
2194        assert_eq!(bucket_overview.likely_file_reads_avoided, 0);
2195        assert_eq!(
2196            bucket_overview.read_avoidance_confidence,
2197            READ_AVOIDANCE_CONFIDENCE_NOT_RECORDED
2198        );
2199    }
2200
2201    #[test]
2202    fn non_file_read_navigation_events_do_not_increment_read_avoidance() {
2203        let overview = TokenOverview::from_events(&[
2204            usage_from_estimates("s", "overview", None, None, 100, 20),
2205            usage_from_estimates("s", "folders", None, None, 100, 20),
2206            usage_from_estimates("s", "files", None, None, 100, 20),
2207            usage_from_estimates("s", "mcp.atlas_health", None, None, 100, 20),
2208            usage_from_estimates("s", "mcp.atlas_purpose_queue", None, None, 100, 20),
2209        ]);
2210
2211        assert_eq!(overview.modeled_file_reads_avoided, 0);
2212        assert_eq!(overview.likely_file_reads_avoided, 0);
2213    }
2214
2215    #[test]
2216    fn zero_baseline_events_do_not_increment_read_avoidance() {
2217        let overview = TokenOverview::from_events(&[
2218            usage_from_estimates("s", "search", None, None, 0, 20),
2219            usage_from_text("s", "summary", None, None, "", "summary"),
2220        ]);
2221
2222        assert_eq!(overview.observed_file_read_replacements, 0);
2223        assert_eq!(overview.modeled_file_reads_avoided, 0);
2224        assert_eq!(overview.likely_file_reads_avoided, 0);
2225    }
2226
2227    #[test]
2228    fn average_policy_halves_only_deduped_directory_walk_baselines() {
2229        let mut first_folder =
2230            usage_from_estimates("s", "folders", Some("src".to_string()), None, 101, 20);
2231        first_folder.denominator_kind = TOKEN_BASELINE_DIRECTORY_WALK.to_string();
2232        first_folder.baseline_identity = "directory:src".to_string();
2233        first_folder.baseline_fingerprint = "directory:src@1".to_string();
2234        let mut second_folder = first_folder.clone();
2235        second_folder.estimated_tokens_with_projectatlas = Some(10);
2236        second_folder.estimated_tokens_saved = Some(91);
2237
2238        let overview = TokenOverview::from_events(&[
2239            usage_from_text(
2240                "s",
2241                "summary",
2242                Some("src/lib.rs".to_string()),
2243                None,
2244                "abcdabcd",
2245                "ab",
2246            ),
2247            first_folder,
2248            second_folder,
2249            usage_from_estimates("s", "search", None, Some("token".to_string()), 80, 20),
2250        ]);
2251
2252        assert_eq!(overview.measured_tokens_saved, 1);
2253        assert_eq!(overview.gross_modeled_tokens_avoided, 232);
2254        assert_eq!(overview.deduped_modeled_tokens_avoided, 131);
2255        assert_eq!(overview.average_modeled_tokens_avoided, 80);
2256        assert_eq!(overview.average_tokens_avoided, 81);
2257        assert_eq!(overview.maximum_tokens_avoided, 132);
2258        assert_eq!(overview.tokens_avoided, overview.average_tokens_avoided);
2259        assert_eq!(overview.repeated_baselines_deduped, 1);
2260    }
2261
2262    #[test]
2263    fn average_directory_walk_policy_preserves_signed_payload_cost() {
2264        let mut event = usage_from_estimates("s", "folders", Some("src".to_string()), None, 5, 4);
2265        event.denominator_kind = TOKEN_BASELINE_DIRECTORY_WALK.to_string();
2266        let overview = TokenOverview::from_events(&[event]);
2267
2268        assert_eq!(overview.average_modeled_tokens_avoided, -2);
2269        assert_eq!(overview.average_tokens_avoided, -2);
2270        assert_eq!(overview.maximum_tokens_avoided, 1);
2271        assert_eq!(overview.tokens_avoided, -2);
2272    }
2273
2274    #[test]
2275    fn raw_accounting_narrows_once_after_wide_signed_aggregation() {
2276        let bound = isize::MAX as usize;
2277        let mut events = vec![
2278            usage_from_estimates("s", "search", None, Some("a".to_string()), bound, 0),
2279            usage_from_estimates("s", "search", None, Some("b".to_string()), bound, 0),
2280            usage_from_estimates("s", "search", None, Some("c".to_string()), 0, bound),
2281        ];
2282        for event in &mut events {
2283            event.dedupe_scope = TOKEN_DEDUPE_SCOPE_EVENT.to_string();
2284        }
2285
2286        let overview = TokenOverview::from_events(&events);
2287
2288        assert_eq!(overview.gross_modeled_tokens_avoided, isize::MAX);
2289        assert_eq!(overview.deduped_modeled_tokens_avoided, isize::MAX);
2290        assert_eq!(overview.average_modeled_tokens_avoided, isize::MAX);
2291        assert_eq!(overview.average_tokens_avoided, isize::MAX);
2292        assert_eq!(overview.maximum_tokens_avoided, isize::MAX);
2293    }
2294
2295    #[test]
2296    fn bucket_accounting_narrows_once_after_wide_signed_aggregation() {
2297        let modeled_bucket = |saved| {
2298            let mut bucket = TokenOverview::from_estimated_totals(1, 1, 1)
2299                .buckets
2300                .remove(0);
2301            bucket.estimated_saved = saved;
2302            bucket
2303        };
2304        let expected = isize::MAX - 1;
2305        for order in [
2306            [isize::MAX, isize::MAX, isize::MIN],
2307            [isize::MAX, isize::MIN, isize::MAX],
2308        ] {
2309            let overview = TokenOverview::from_buckets(
2310                order.into_iter().map(&modeled_bucket).collect::<Vec<_>>(),
2311            );
2312            assert_eq!(overview.gross_modeled_tokens_avoided, expected);
2313            assert_eq!(overview.average_modeled_tokens_avoided, expected);
2314            assert_eq!(overview.average_tokens_avoided, expected);
2315            assert_eq!(overview.maximum_tokens_avoided, expected);
2316        }
2317    }
2318
2319    #[test]
2320    fn overview_dedupes_repeated_modeled_baselines_without_hiding_measured_savings() {
2321        let overview = TokenOverview::from_events(&[
2322            usage_from_text(
2323                "s",
2324                "summary",
2325                Some("src/lib.rs".to_string()),
2326                None,
2327                "abcdabcd",
2328                "ab",
2329            ),
2330            usage_from_estimates("s", "search", None, Some("token".to_string()), 400, 40),
2331            usage_from_estimates("s", "search", None, Some("token".to_string()), 400, 30),
2332            usage_from_estimates("s", "search", None, Some("token".to_string()), 400, 20),
2333        ]);
2334
2335        assert_eq!(overview.estimated_saved, 1111);
2336        assert_eq!(overview.legacy_gross_estimated_saved, 1111);
2337        assert_eq!(overview.measured_tokens_saved, 1);
2338        assert_eq!(overview.gross_modeled_tokens_avoided, 1110);
2339        assert_eq!(overview.deduped_modeled_tokens_avoided, 310);
2340        assert_eq!(overview.tokens_avoided, 311);
2341        assert_eq!(overview.repeated_baselines_deduped, 2);
2342        assert_eq!(overview.observed_file_read_replacements, 1);
2343        assert_eq!(overview.modeled_file_reads_avoided, 3);
2344        assert_eq!(overview.likely_file_reads_avoided, 4);
2345    }
2346}