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