1use anyhow::{Result, anyhow, bail};
2use clap::{ArgMatches, Command};
3
4use crate::core::savings_ledger::event::SavingsEvent;
5use crate::core::{ocla::OclaService, ocla_bus, savings_ledger};
6
7type TraitEntry = (
10 &'static str,
11 for<'a> fn(&'a crate::core::ocla::OclaRegistry) -> &'a dyn OclaService,
12);
13
14const TRAITS: [TraitEntry; 14] = [
15 ("observation_hook", |r| r.observation_hook.as_ref()),
16 ("usage_sink", |r| r.usage_sink.as_ref()),
17 ("metrics_exporter", |r| r.metrics_exporter.as_ref()),
18 ("savings_ledger", |r| r.savings_ledger.as_ref()),
19 ("intent_classifier", |r| r.intent_classifier.as_ref()),
20 ("outcome_tracker", |r| r.outcome_tracker.as_ref()),
21 ("compression_provider", |r| r.compression_provider.as_ref()),
22 ("response_optimizer", |r| r.response_optimizer.as_ref()),
23 ("model_router", |r| r.model_router.as_ref()),
24 ("efficiency_analyzer", |r| r.efficiency_analyzer.as_ref()),
25 ("config_tuner", |r| r.config_tuner.as_ref()),
26 ("experiment_runner", |r| r.experiment_runner.as_ref()),
27 ("connector_scheduler", |r| r.connector_scheduler.as_ref()),
28 ("agent_gateway", |r| r.agent_gateway.as_ref()),
29];
30
31pub fn register(app: Command) -> Command {
32 app.subcommand(
33 Command::new("ocla")
34 .about("Inspect Open Context & Token Lifecycle Architecture state")
35 .subcommand(Command::new("status").about("Show OCLA status and ledger coverage"))
36 .subcommand(
37 Command::new("reconcile")
38 .about("Reconcile legacy and unified savings ledgers")
39 .arg(
40 clap::Arg::new("json")
41 .long("json")
42 .action(clap::ArgAction::SetTrue),
43 ),
44 )
45 .subcommand(
46 Command::new("ledger")
47 .about("Inspect the savings ledger")
48 .subcommand(Command::new("summary").about("Per-mechanism breakdown"))
49 .subcommand(Command::new("verify").about("Verify hash chain integrity"))
50 .subcommand(
51 Command::new("query")
52 .about("List events by mechanism")
53 .arg(clap::Arg::new("mechanism").long("mechanism").required(true))
54 .arg(
55 clap::Arg::new("limit")
56 .long("limit")
57 .default_value("10")
58 .value_parser(clap::value_parser!(usize)),
59 ),
60 )
61 .subcommand(
62 Command::new("p5-coverage").about("Show P5 field population stats"),
63 ),
64 ),
65 )
66}
67
68pub fn handle(matches: &ArgMatches) -> Result<()> {
69 match matches.subcommand() {
70 Some(("ocla", nested)) => return handle(nested),
71 Some(("status", _)) | None => print_status(),
72 Some(("reconcile", nested)) => return handle_reconcile(nested),
73 Some(("ledger", nested)) => return handle_ledger(nested),
74 Some((name, _)) => bail!("unknown ocla subcommand: {name}"),
75 }
76 Ok(())
77}
78
79fn handle_reconcile(matches: &ArgMatches) -> Result<()> {
80 let ledger = crate::core::ocla::unified_ledger::FileUnifiedLedger::from_data_dir()
81 .map_err(|error| anyhow!(error.to_string()))?;
82 let report = ledger
83 .reconcile()
84 .map_err(|error| anyhow!(error.to_string()))?;
85
86 if matches.get_flag("json") {
87 println!("{}", serde_json::to_string_pretty(&report)?);
88 } else {
89 println!("Reconciliation report");
90 println!(" Metric Value");
91 println!(" ------------------- -----");
92 println!(" Matched {}", report.matched);
93 println!(" Unmatched legacy {}", report.unmatched_legacy);
94 println!(" Unmatched unified {}", report.unmatched_unified);
95 println!(" Token drift {}", report.token_drift);
96 println!(" Double-bookings {}", report.double_bookings.len());
97 for hash in &report.double_bookings {
98 println!(" {hash}");
99 }
100 }
101
102 Ok(())
103}
104
105fn print_status() {
106 let registry = crate::core::ocla::OclaRegistry::global();
107 println!("OCLA traits:");
108 for (name, service) in TRAITS {
109 let capability = service(registry).capability();
110 println!(" {name}: builtin ({:?})", capability.status);
111 }
112
113 println!(
114 "OclaBus: {} (total events emitted: {})",
115 if ocla_bus::is_enabled() {
116 "enabled"
117 } else {
118 "disabled"
119 },
120 ocla_bus::total_emitted()
121 );
122
123 let path = savings_ledger::store::default_path();
124 let summary = path
125 .as_deref()
126 .map(savings_ledger::store::summarize)
127 .unwrap_or_default();
128 println!(
129 "Ledger: total events={}, saved tokens={}, saved USD={:.6}",
130 summary.total_events, summary.saved_tokens, summary.saved_usd
131 );
132
133 let events = path
134 .as_deref()
135 .map(savings_ledger::store::load)
136 .unwrap_or_default();
137 println!("P5 field coverage (with / without):");
138 println!(
139 " measurement_method: {} / {}",
140 events
141 .iter()
142 .filter(|e| e.measurement_method.is_some())
143 .count(),
144 events
145 .iter()
146 .filter(|e| e.measurement_method.is_none())
147 .count()
148 );
149 println!(
150 " evidence_class: {} / {}",
151 events.iter().filter(|e| e.evidence_class.is_some()).count(),
152 events.iter().filter(|e| e.evidence_class.is_none()).count()
153 );
154 println!(
155 " attribution_id: {} / {}",
156 events.iter().filter(|e| e.attribution_id.is_some()).count(),
157 events.iter().filter(|e| e.attribution_id.is_none()).count()
158 );
159}
160
161pub fn cmd_ocla(args: &[String]) {
163 let mut argv = vec!["ocla".to_string()];
164 argv.extend(args.iter().cloned());
165 let matches = register(Command::new("lean-ctx"))
166 .try_get_matches_from(argv)
167 .unwrap_or_else(|error| error.exit());
168 handle(&matches).unwrap_or_else(|error| {
169 eprintln!("ocla: {error}");
170 std::process::exit(2);
171 });
172}
173
174const MECHANISMS: [&str; 3] = ["compression", "routing", "caching"];
177const P5_FIELDS: [&str; 19] = [
178 "intent_tag",
179 "outcome",
180 "model_original",
181 "model_routed",
182 "routing_savings",
183 "response_original_tokens",
184 "response_delivered_tokens",
185 "agent_chain_id",
186 "chain_depth",
187 "measurement_method",
188 "evidence_class",
189 "confidence",
190 "quality_signal",
191 "attribution_group",
192 "attribution_id",
193 "baseline_ref",
194 "price_version",
195 "customer_approval",
196 "settlement_status",
197];
198
199fn handle_ledger(matches: &ArgMatches) -> Result<()> {
200 let path =
201 savings_ledger::store::default_path().ok_or_else(|| anyhow!("ledger path unavailable"))?;
202 let action = matches.subcommand_name().unwrap_or("summary");
203
204 match action {
205 "summary" => {
206 let summary = savings_ledger::store::summarize(&path);
207 let events = savings_ledger::store::load(&path);
208 println!("Ledger events: {}", summary.total_events);
209 for (mechanism, count, tokens, usd) in mechanism_breakdown(&events, &summary) {
210 println!("{mechanism}: {count} events, {tokens} tokens, ${usd:.6}");
211 }
212 }
213 "verify" => {
214 let result = savings_ledger::store::verify(&path);
215 if result.valid {
216 println!("Ledger valid: {} events", result.total);
217 } else {
218 println!(
219 "Ledger invalid at event {} ({} events read)",
220 result.first_invalid_at.unwrap_or(result.total),
221 result.total
222 );
223 }
224 }
225 "query" => {
226 let mechanism = matches
227 .get_one::<String>("mechanism")
228 .ok_or_else(|| anyhow!("query requires --mechanism <M>"))?;
229 let limit = matches.get_one::<usize>("limit").copied().unwrap_or(10);
230 for event in events_for_mechanism(&savings_ledger::store::load(&path), mechanism, limit)
231 {
232 println!(
233 "{} {} {} tokens={} usd=${:.6} hash={}",
234 event.ts,
235 event.mechanism,
236 event.tool,
237 event.saved_tokens,
238 event.saved_usd,
239 event.entry_hash
240 );
241 }
242 }
243 "p5-coverage" => {
244 let events = savings_ledger::store::load(&path);
245 let events_with_p5 = events
246 .iter()
247 .filter(|event| p5_presence(event).iter().any(|populated| *populated))
248 .count();
249 println!("P5 coverage: {} events", events.len());
250 println!(
251 "Events with any P5 field: {events_with_p5}/{}",
252 events.len()
253 );
254 for (field, populated) in P5_FIELDS.iter().zip(p5_counts(&events)) {
255 println!("{field}: {populated}/{}", events.len());
256 }
257 }
258 other => return Err(anyhow!("unknown ledger subcommand: {other}")),
259 }
260 Ok(())
261}
262
263fn mechanism_breakdown(
264 events: &[SavingsEvent],
265 summary: &savings_ledger::store::LedgerSummary,
266) -> Vec<(&'static str, usize, u64, f64)> {
267 MECHANISMS
268 .iter()
269 .map(|mechanism| {
270 let count = events
271 .iter()
272 .filter(|event| event.mechanism == *mechanism)
273 .count();
274 let (tokens, usd) = summary
275 .by_mechanism
276 .iter()
277 .find(|row| row.0 == *mechanism)
278 .map_or((0, 0.0), |row| (row.1, row.2));
279 (*mechanism, count, tokens, usd)
280 })
281 .collect()
282}
283
284fn events_for_mechanism<'a>(
285 events: &'a [SavingsEvent],
286 mechanism: &str,
287 limit: usize,
288) -> impl Iterator<Item = &'a SavingsEvent> {
289 events
290 .iter()
291 .filter(move |event| event.mechanism == mechanism)
292 .rev()
293 .take(limit)
294}
295
296fn p5_counts(events: &[SavingsEvent]) -> [usize; 19] {
297 let mut counts = [0; 19];
298 for event in events {
299 let populated = p5_presence(event);
300 for (count, is_populated) in counts.iter_mut().zip(populated) {
301 *count += usize::from(is_populated);
302 }
303 }
304 counts
305}
306
307fn p5_presence(event: &SavingsEvent) -> [bool; 19] {
308 [
309 event.intent_tag.is_some(),
310 event.outcome.is_some(),
311 event.model_original.is_some(),
312 event.model_routed.is_some(),
313 event.routing_savings.is_some(),
314 event.response_original_tokens.is_some(),
315 event.response_delivered_tokens.is_some(),
316 event.agent_chain_id.is_some(),
317 event.chain_depth.is_some(),
318 event.measurement_method.is_some(),
319 event.evidence_class.is_some(),
320 event.confidence.is_some(),
321 event.quality_signal.is_some(),
322 event.attribution_group.is_some(),
323 event.attribution_id.is_some(),
324 event.baseline_ref.is_some(),
325 event.price_version.is_some(),
326 event.customer_approval.is_some(),
327 event.settlement_status.is_some(),
328 ]
329}
330
331#[cfg(test)]
334mod tests {
335 use super::*;
336 use crate::core::savings_ledger::event::MECHANISM_COMPRESSION;
337
338 #[test]
339 fn register_accepts_status() {
340 let matches = register(Command::new("lean-ctx"))
341 .try_get_matches_from(["lean-ctx", "ocla", "status"])
342 .expect("status should parse");
343 let (_, ocla) = matches.subcommand().expect("ocla subcommand");
344 assert!(matches!(ocla.subcommand_name(), Some("status")));
345 }
346
347 #[test]
348 fn register_accepts_reconcile_json() {
349 let matches = register(Command::new("lean-ctx"))
350 .try_get_matches_from(["lean-ctx", "ocla", "reconcile", "--json"])
351 .expect("reconcile should parse");
352 let (_, ocla) = matches.subcommand().expect("ocla subcommand");
353 let (_, reconcile) = ocla.subcommand().expect("reconcile subcommand");
354 assert!(reconcile.get_flag("json"));
355 }
356
357 fn event(mechanism: &str, saved_tokens: u64) -> SavingsEvent {
358 serde_json::from_value(serde_json::json!({
359 "ts": "2026-07-20T00:00:00Z",
360 "tool": "ctx_read",
361 "mechanism": mechanism,
362 "model_id": "test",
363 "tokenizer": "o200k_base",
364 "baseline_tokens": saved_tokens + 10,
365 "actual_tokens": 10,
366 "saved_tokens": saved_tokens,
367 "bounce_adjustment": 0,
368 "unit_price_per_m_usd": 1.0,
369 "saved_usd": 0.001,
370 "repo_hash": "repo",
371 "agent_id": "agent",
372 "prev_hash": "genesis",
373 "entry_hash": "hash",
374 "version": "5"
375 }))
376 .expect("valid test event")
377 }
378
379 #[test]
380 fn p5_counts_only_populated_fields() {
381 let mut populated = event(MECHANISM_COMPRESSION, 10);
382 populated.intent_tag = Some("coding".into());
383 populated.confidence = Some(0.9);
384 let counts = p5_counts(&[populated, event("routing", 0)]);
385 assert_eq!(counts[0], 1);
386 assert_eq!(counts[11], 1);
387 assert!(counts.iter().skip(1).take(10).all(|count| *count == 0));
388 }
389
390 #[test]
391 fn query_returns_newest_matching_events_and_honors_limit() {
392 let events = vec![
393 event(MECHANISM_COMPRESSION, 1),
394 event("routing", 2),
395 event(MECHANISM_COMPRESSION, 3),
396 ];
397 let result: Vec<u64> = events_for_mechanism(&events, MECHANISM_COMPRESSION, 1)
398 .map(|event| event.saved_tokens)
399 .collect();
400 assert_eq!(result, vec![3]);
401 }
402}