cctally 1.69.0 → 1.69.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2050 @@
1
+ """SQLite adapter for S3's qualified Codex accounting foundation."""
2
+ from __future__ import annotations
3
+
4
+ import datetime as dt
5
+ import hashlib
6
+ import json
7
+ import pathlib
8
+ import re
9
+ import sqlite3
10
+ import sys
11
+ from copy import copy
12
+ from collections import defaultdict
13
+ from typing import Iterable
14
+
15
+ from _cctally_core import (
16
+ _command_as_of,
17
+ compute_week_bounds,
18
+ get_week_start_name,
19
+ parse_iso_datetime,
20
+ )
21
+ from _cctally_cache import _codex_provider_roots
22
+ from _lib_quota import build_blocks
23
+ from _cctally_quota import load_codex_quota_observations
24
+ from _lib_source_analytics import (
25
+ AnalyticsWindow,
26
+ QUALIFIED_METADATA_WARNING,
27
+ QualifiedCodexEntry,
28
+ SourceResult,
29
+ build_codex_diff_result,
30
+ build_codex_project_result,
31
+ build_codex_range_result,
32
+ build_codex_report_result,
33
+ build_codex_reuse_result,
34
+ assign_collision_safe_project_labels,
35
+ emitted_project_label,
36
+ opaque_project_key,
37
+ resolve_codex_diff_normalization,
38
+ source_result_wire,
39
+ )
40
+
41
+
42
+ UTC = dt.timezone.utc
43
+
44
+
45
+ class QualifiedMetadataUnavailable(RuntimeError):
46
+ """S1-qualified metadata is absent or cannot be truthfully read."""
47
+
48
+
49
+ class SourceUsageError(ValueError):
50
+ """A provider-aware request is syntactically or semantically invalid."""
51
+
52
+
53
+ _OPAQUE_PROJECT_KEY_RE = re.compile(r"^project:[0-9a-f]{24}$")
54
+
55
+
56
+ _QUALIFIED_CODEX_ENTRIES_SQL = """
57
+ SELECT entries.timestamp_utc, entries.session_id, entries.source_path,
58
+ entries.source_root_key,
59
+ entries.conversation_key, entries.model,
60
+ entries.input_tokens, entries.cached_input_tokens,
61
+ entries.output_tokens, entries.reasoning_output_tokens,
62
+ entries.total_tokens, threads.cwd, threads.git_json,
63
+ threads.conversation_key AS joined_conversation_key,
64
+ threads.source_root_key AS joined_source_root_key
65
+ FROM codex_session_entries AS entries
66
+ INDEXED BY idx_codex_entries_ts_root_conversation
67
+ LEFT JOIN codex_conversation_threads AS threads
68
+ ON threads.conversation_key = entries.conversation_key
69
+ AND threads.source_root_key = entries.source_root_key
70
+ WHERE entries.timestamp_utc >= ?
71
+ AND entries.timestamp_utc < ?
72
+ ORDER BY entries.timestamp_utc ASC, entries.source_root_key ASC,
73
+ entries.conversation_key ASC, entries.id ASC
74
+ """
75
+
76
+
77
+ _CODEX_ACCOUNTING_ENTRIES_SQL = """
78
+ SELECT timestamp_utc, source_root_key, conversation_key, model,
79
+ input_tokens, cached_input_tokens, output_tokens,
80
+ reasoning_output_tokens, total_tokens
81
+ FROM codex_session_entries
82
+ WHERE timestamp_utc >= ?
83
+ AND timestamp_utc < ?
84
+ AND source_root_key IS NOT NULL
85
+ ORDER BY timestamp_utc ASC, source_root_key ASC, conversation_key ASC, id ASC
86
+ """
87
+
88
+
89
+ def _cctally():
90
+ return sys.modules["cctally"]
91
+
92
+
93
+ def _parse_timestamp(value: object) -> dt.datetime:
94
+ try:
95
+ timestamp = dt.datetime.fromisoformat(str(value).replace("Z", "+00:00"))
96
+ except (TypeError, ValueError) as exc:
97
+ raise QualifiedMetadataUnavailable("Codex accounting metadata is unavailable") from exc
98
+ if timestamp.tzinfo is None or timestamp.utcoffset() is None:
99
+ raise QualifiedMetadataUnavailable("Codex accounting metadata is unavailable")
100
+ return timestamp.astimezone(UTC)
101
+
102
+
103
+ def _project_label(value: object) -> str:
104
+ """Render only a basename-like label; absolute paths remain internal."""
105
+ raw = str(value).strip()
106
+ normalized = raw.replace("\\", "/")
107
+ if normalized in {"", "/"}:
108
+ return "(root)" if normalized == "/" else "(unassigned)"
109
+ parts = tuple(part for part in normalized.split("/") if part)
110
+ # A cwd that is exactly a user home directory must never turn the local
111
+ # account name into a public project label. Keep the generic token too so
112
+ # it remains safe for fixture homes and Windows rollouts.
113
+ if (
114
+ len(parts) == 2 and parts[0] in {"Users", "home"}
115
+ ) or (
116
+ len(parts) == 3 and parts[-2] in {"Users", "home"}
117
+ ):
118
+ return "(home)"
119
+ return parts[-1] if parts else "(unassigned)"
120
+
121
+
122
+ def _git_resolved_key(value: object) -> str | None:
123
+ """Return a non-renderable identity for valid S1 git metadata."""
124
+ if not isinstance(value, str) or not value:
125
+ return None
126
+ try:
127
+ decoded = json.loads(value)
128
+ except (TypeError, ValueError):
129
+ return None
130
+ if not isinstance(decoded, dict) or not decoded:
131
+ return None
132
+ canonical = json.dumps(decoded, sort_keys=True, separators=(",", ":"), allow_nan=False)
133
+ return "git:" + hashlib.sha256(canonical.encode("utf-8")).hexdigest()
134
+
135
+
136
+ def _require_joined_metadata(row: sqlite3.Row) -> tuple[str, str]:
137
+ root_key = row["source_root_key"]
138
+ conversation_key = row["conversation_key"]
139
+ if (
140
+ not isinstance(root_key, str) or not root_key
141
+ or not isinstance(conversation_key, str) or not conversation_key
142
+ or row["joined_conversation_key"] != conversation_key
143
+ or row["joined_source_root_key"] != root_key
144
+ ):
145
+ raise QualifiedMetadataUnavailable("Codex qualified project metadata is unavailable")
146
+ return root_key, conversation_key
147
+
148
+
149
+ def load_qualified_codex_entries(
150
+ start: dt.datetime,
151
+ end: dt.datetime,
152
+ *,
153
+ speed: str,
154
+ sync: bool = True,
155
+ group: str = "git-root",
156
+ cache_conn: sqlite3.Connection | None = None,
157
+ ) -> tuple[QualifiedCodexEntry, ...]:
158
+ """Load exactly one bounded, root-qualified Codex accounting read.
159
+
160
+ The S1 cache is the sole metadata source. Unlike unqualified Codex
161
+ accounting readers, this adapter deliberately has no direct-rollout fallback:
162
+ without the relational conversation join it cannot safely attribute projects.
163
+ """
164
+ if start.tzinfo is None or start.utcoffset() is None:
165
+ raise ValueError("start must be timezone-aware")
166
+ if end.tzinfo is None or end.utcoffset() is None:
167
+ raise ValueError("end must be timezone-aware")
168
+ if end <= start:
169
+ raise ValueError("end must be after start")
170
+
171
+ if cache_conn is not None and sync:
172
+ raise ValueError("cache_conn requires sync=False")
173
+
174
+ c = _cctally()
175
+ owns_conn = cache_conn is None
176
+ if owns_conn:
177
+ try:
178
+ conn = c.open_cache_db()
179
+ except (OSError, sqlite3.Error) as exc:
180
+ raise QualifiedMetadataUnavailable("Codex qualified project metadata is unavailable") from exc
181
+ else:
182
+ conn = cache_conn
183
+ previous_row_factory = conn.row_factory
184
+ try:
185
+ if sync:
186
+ stats = c.sync_codex_cache(conn)
187
+ if stats.lock_contended:
188
+ raise QualifiedMetadataUnavailable("Codex qualified project metadata is unavailable")
189
+ conn.row_factory = sqlite3.Row
190
+ rows = tuple(conn.execute(
191
+ _QUALIFIED_CODEX_ENTRIES_SQL,
192
+ (start.astimezone(UTC).isoformat(), end.astimezone(UTC).isoformat()),
193
+ ))
194
+ except sqlite3.Error as exc:
195
+ raise QualifiedMetadataUnavailable("Codex qualified project metadata is unavailable") from exc
196
+ finally:
197
+ if owns_conn:
198
+ conn.close()
199
+ else:
200
+ conn.row_factory = previous_row_factory
201
+
202
+ resolver_cache: dict[object, object] = {}
203
+ resolved_by_cwd: dict[str, object] = {}
204
+ resolved_by_git_json: dict[str, str | None] = {}
205
+ result: list[QualifiedCodexEntry] = []
206
+ for row in rows:
207
+ root_key, conversation_key = _require_joined_metadata(row)
208
+ cwd = row["cwd"]
209
+ if isinstance(cwd, str) and cwd:
210
+ project = resolved_by_cwd.get(cwd)
211
+ if project is None:
212
+ project = c._resolve_project_key(cwd, group, resolver_cache)
213
+ resolved_by_cwd[cwd] = project
214
+ resolved_key = project.bucket_path
215
+ cwd_label = _project_label(cwd)
216
+ project_label = (
217
+ cwd_label if cwd_label in {"(home)", "(root)"}
218
+ else _project_label(project.display_key)
219
+ )
220
+ else:
221
+ git_json = row["git_json"]
222
+ if isinstance(git_json, str) and git_json not in resolved_by_git_json:
223
+ resolved_by_git_json[git_json] = _git_resolved_key(git_json)
224
+ git_key = resolved_by_git_json.get(git_json) if isinstance(git_json, str) else None
225
+ if git_key is None:
226
+ resolved_key = "(unassigned)"
227
+ project_label = "(unassigned)"
228
+ else:
229
+ resolved_key = git_key
230
+ project_label = "Git project"
231
+ try:
232
+ cost_usd = c._calculate_codex_entry_cost(
233
+ str(row["model"]), int(row["input_tokens"]),
234
+ int(row["cached_input_tokens"]), int(row["output_tokens"]),
235
+ int(row["reasoning_output_tokens"]), speed=speed,
236
+ )
237
+ except (TypeError, ValueError, OverflowError) as exc:
238
+ raise QualifiedMetadataUnavailable("Codex qualified accounting is unavailable") from exc
239
+ result.append(QualifiedCodexEntry(
240
+ timestamp=_parse_timestamp(row["timestamp_utc"]),
241
+ session_id=str(row["session_id"] or ""),
242
+ source_path=str(row["source_path"] or ""),
243
+ source_root_key=root_key,
244
+ conversation_key=conversation_key,
245
+ project_key=opaque_project_key("codex", root_key, resolved_key),
246
+ project_label=project_label,
247
+ model=str(row["model"]),
248
+ input_tokens=int(row["input_tokens"]),
249
+ cached_input_tokens=int(row["cached_input_tokens"]),
250
+ output_tokens=int(row["output_tokens"]),
251
+ reasoning_output_tokens=int(row["reasoning_output_tokens"]),
252
+ total_tokens=int(row["total_tokens"]),
253
+ cost_usd=cost_usd,
254
+ ))
255
+ return tuple(result)
256
+
257
+
258
+ def load_codex_accounting_entries(
259
+ start: dt.datetime, end: dt.datetime, *, speed: str, sync: bool = True,
260
+ force_direct: bool = False,
261
+ ) -> tuple[QualifiedCodexEntry, ...]:
262
+ """Load accounting through the canonical cache-first Codex reader.
263
+
264
+ Unlike qualified project attribution, accounting remains useful when the
265
+ S1 cache is unavailable or its ingest lock is contended. The established
266
+ reader owns that cache/direct-JSONL fallback; this adapter only converts its
267
+ native accounting objects to the provider result contract. It never reads
268
+ rollout metadata for a project identity.
269
+ """
270
+ if start.tzinfo is None or start.utcoffset() is None:
271
+ raise ValueError("start must be timezone-aware")
272
+ if end.tzinfo is None or end.utcoffset() is None:
273
+ raise ValueError("end must be timezone-aware")
274
+ if end <= start:
275
+ raise ValueError("end must be after start")
276
+ c = _cctally()
277
+ try:
278
+ entries = (
279
+ c._collect_codex_entries_direct(start, end)
280
+ if force_direct else c.get_codex_entries(start, end, skip_sync=not sync)
281
+ )
282
+ except (OSError, sqlite3.Error, ValueError) as exc:
283
+ raise RuntimeError("Codex accounting is unavailable") from exc
284
+
285
+ roots = tuple(_codex_provider_roots())
286
+
287
+ def root_key_for_path(source_path: object) -> str:
288
+ path = pathlib.Path(str(source_path)).expanduser()
289
+ try:
290
+ resolved = path.resolve()
291
+ except OSError:
292
+ resolved = path.absolute()
293
+ for root in roots:
294
+ try:
295
+ resolved.relative_to(root.provider_root)
296
+ except ValueError:
297
+ continue
298
+ return root.source_root_key
299
+ return "direct:" + hashlib.sha256(str(resolved.parent).encode("utf-8")).hexdigest()[:24]
300
+
301
+ result: list[QualifiedCodexEntry] = []
302
+ for entry in entries:
303
+ timestamp = getattr(entry, "timestamp", None)
304
+ if not isinstance(timestamp, dt.datetime):
305
+ continue
306
+ timestamp = _parse_timestamp(timestamp.isoformat())
307
+ if not start <= timestamp < end:
308
+ continue
309
+ source_path = str(getattr(entry, "source_path", ""))
310
+ root_key = root_key_for_path(source_path)
311
+ try:
312
+ cost_usd = c._calculate_codex_entry_cost(
313
+ str(entry.model), int(entry.input_tokens),
314
+ int(entry.cached_input_tokens), int(entry.output_tokens),
315
+ int(entry.reasoning_output_tokens), speed=speed,
316
+ )
317
+ except (TypeError, ValueError, OverflowError) as exc:
318
+ raise RuntimeError("Codex accounting is unavailable") from exc
319
+ native_session = str(getattr(entry, "session_id", "") or "accounting")
320
+ conversation_key = "accounting:" + hashlib.sha256(
321
+ f"{root_key}\0{native_session}\0{source_path}".encode("utf-8")
322
+ ).hexdigest()[:24]
323
+ result.append(QualifiedCodexEntry(
324
+ timestamp=timestamp,
325
+ session_id=native_session,
326
+ source_path=source_path,
327
+ source_root_key=root_key,
328
+ conversation_key=conversation_key,
329
+ project_key=opaque_project_key("codex", root_key, "(unassigned)"),
330
+ project_label="(unassigned)",
331
+ model=str(entry.model),
332
+ input_tokens=int(entry.input_tokens),
333
+ cached_input_tokens=int(entry.cached_input_tokens),
334
+ output_tokens=int(entry.output_tokens),
335
+ reasoning_output_tokens=int(entry.reasoning_output_tokens),
336
+ total_tokens=int(entry.total_tokens),
337
+ cost_usd=cost_usd,
338
+ ))
339
+ return tuple(result)
340
+
341
+
342
+ def _identity_sort_key(identity: object) -> tuple[str, str, str, str, int]:
343
+ return (
344
+ str(identity.source), str(identity.source_root_key),
345
+ str(identity.logical_limit_key), str(identity.observed_slot),
346
+ int(identity.window_minutes),
347
+ )
348
+
349
+
350
+ def select_codex_report_blocks(blocks: Iterable[object], *, weeks: int) -> tuple[object, ...]:
351
+ """Select newest blocks per full native identity, never by display slot."""
352
+ if not isinstance(weeks, int) or isinstance(weeks, bool) or weeks <= 0:
353
+ raise ValueError("weeks must be a positive integer")
354
+ by_identity: dict[object, list[object]] = defaultdict(list)
355
+ for block in blocks:
356
+ by_identity[block.identity].append(block)
357
+ selected: list[object] = []
358
+ for identity in sorted(by_identity, key=_identity_sort_key):
359
+ newest = sorted(
360
+ by_identity[identity],
361
+ key=lambda block: (block.resets_at, block.nominal_start_at),
362
+ reverse=True,
363
+ )[:weeks]
364
+ selected.extend(sorted(newest, key=lambda block: (block.resets_at, block.nominal_start_at)))
365
+ return tuple(selected)
366
+
367
+
368
+ def select_codex_project_blocks(
369
+ blocks: Iterable[object], *, range_start: dt.datetime, range_end: dt.datetime,
370
+ as_of: dt.datetime,
371
+ ) -> tuple[object, ...]:
372
+ """Keep only native blocks that overlap one requested project interval.
373
+
374
+ Project attribution is root- and logical-limit-qualified. A block is
375
+ applicable only when its observed native interval overlaps the requested
376
+ accounting interval; adjacent or future blocks must not create a fictional
377
+ quota child on a project row.
378
+ """
379
+ start = range_start.astimezone(UTC)
380
+ end = range_end.astimezone(UTC)
381
+ cutoff = as_of.astimezone(UTC)
382
+ selected: list[object] = []
383
+ for block in blocks:
384
+ identity = getattr(block, "identity", None)
385
+ if getattr(identity, "source", None) != "codex":
386
+ continue
387
+ nominal_start = getattr(block, "nominal_start_at", None)
388
+ resets_at = getattr(block, "resets_at", None)
389
+ if not isinstance(nominal_start, dt.datetime) or not isinstance(resets_at, dt.datetime):
390
+ continue
391
+ if nominal_start.tzinfo is None or resets_at.tzinfo is None:
392
+ continue
393
+ block_start = nominal_start.astimezone(UTC)
394
+ block_end = min(cutoff, resets_at.astimezone(UTC))
395
+ if block_start < end and block_end > start:
396
+ selected.append(block)
397
+ return tuple(sorted(
398
+ selected,
399
+ key=lambda block: (
400
+ _identity_sort_key(block.identity), block.resets_at, block.nominal_start_at,
401
+ ),
402
+ ))
403
+
404
+
405
+ def _arg_datetime(args: object, primary: str, fallback: str) -> dt.datetime:
406
+ value = getattr(args, primary, getattr(args, fallback, None))
407
+ if not isinstance(value, dt.datetime):
408
+ raise ValueError(f"{primary} must be a resolved datetime")
409
+ return value
410
+
411
+
412
+ def _source_config_and_tz(args: object) -> tuple[dict, object]:
413
+ """Resolve the display configuration once for a source-aware command."""
414
+ c = _cctally()
415
+ config = c._load_claude_config_for_args(args)
416
+ c._bridge_z_into_tz(args, config)
417
+ tz = c.resolve_display_tz(args, config)
418
+ args._resolved_tz = tz
419
+ return config, tz
420
+
421
+
422
+ def _source_date_range(args: object, *, config: dict, tz: object) -> tuple[dt.datetime, dt.datetime]:
423
+ """Resolve shared project date flags to the source path's half-open range."""
424
+ c = _cctally()
425
+ parsed = c._parse_cli_date_range(args, now_utc=_command_as_of())
426
+ if isinstance(parsed, int):
427
+ raise ValueError("invalid date range")
428
+ start, end = parsed
429
+ if getattr(args, "until", None) and not any(
430
+ marker in args.until for marker in ("T", "+", "Z")
431
+ ):
432
+ # The historical helper supplies the final microsecond of a civil
433
+ # date. Provider reads are half-open, so advance to the next midnight.
434
+ end += dt.timedelta(microseconds=1)
435
+ return start.astimezone(UTC), end.astimezone(UTC)
436
+
437
+
438
+ def _resolve_source_project_range(args: object) -> tuple[dt.datetime, dt.datetime]:
439
+ if isinstance(getattr(args, "range_start", None), dt.datetime):
440
+ return _arg_datetime(args, "range_start", "start"), _arg_datetime(args, "range_end", "end")
441
+ c = _cctally()
442
+ config, tz = _source_config_and_tz(args)
443
+ weeks = getattr(args, "weeks", None)
444
+ if weeks is not None and weeks < 1:
445
+ raise ValueError("--weeks must be >= 1")
446
+ if weeks is not None and (getattr(args, "since", None) or getattr(args, "until", None)):
447
+ raise ValueError("--weeks cannot be combined with --since/--until")
448
+ if getattr(args, "since", None) or getattr(args, "until", None):
449
+ start, end = _source_date_range(args, config=config, tz=tz)
450
+ else:
451
+ now = _command_as_of()
452
+ local_now = now.astimezone(tz) if tz is not None else now.astimezone()
453
+ week_start, _ = compute_week_bounds(
454
+ local_now, get_week_start_name(config),
455
+ )
456
+ start = dt.datetime.combine(week_start, dt.time.min, tzinfo=local_now.tzinfo)
457
+ if weeks is not None:
458
+ start -= dt.timedelta(days=7 * (weeks - 1))
459
+ end = now
460
+ args.range_start, args.range_end = start.astimezone(UTC), end.astimezone(UTC)
461
+ args.as_of = end.astimezone(UTC)
462
+ return args.range_start, args.range_end
463
+
464
+
465
+ def _resolve_source_range_cost_range(args: object) -> tuple[dt.datetime, dt.datetime]:
466
+ if isinstance(getattr(args, "start", None), dt.datetime):
467
+ return _arg_datetime(args, "start", "range_start"), _arg_datetime(args, "end", "range_end")
468
+ c = _cctally()
469
+ start = parse_iso_datetime(args.start, "--start")
470
+ end = parse_iso_datetime(args.end, "--end") if getattr(args, "end", None) else _command_as_of()
471
+ if end < start:
472
+ raise ValueError("--end must be after --start")
473
+ args.start, args.end = start.astimezone(UTC), end.astimezone(UTC)
474
+ return args.start, args.end
475
+
476
+
477
+ def _resolve_source_cache_range(args: object) -> tuple[dt.datetime, dt.datetime]:
478
+ if isinstance(getattr(args, "start", None), dt.datetime):
479
+ return _arg_datetime(args, "start", "range_start"), _arg_datetime(args, "end", "range_end")
480
+ c = _cctally()
481
+ _config, tz = _source_config_and_tz(args)
482
+ start, end = c._resolve_cache_report_window(
483
+ args, now_utc=_command_as_of(), tz_name=(tz.key if tz is not None else None),
484
+ )
485
+ if getattr(args, "until", None) and not any(
486
+ marker in args.until for marker in ("T", "+", "Z")
487
+ ):
488
+ end += dt.timedelta(microseconds=1)
489
+ args.start, args.end = start.astimezone(UTC), end.astimezone(UTC)
490
+ return args.start, args.end
491
+
492
+
493
+ def _resolve_source_diff_windows(args: object) -> tuple[AnalyticsWindow, AnalyticsWindow]:
494
+ if isinstance(getattr(args, "window_a", None), AnalyticsWindow):
495
+ return args.window_a, args.window_b
496
+ c = _cctally()
497
+ config, tz = _source_config_and_tz(args)
498
+ now = _command_as_of()
499
+ tz_name = tz.key if tz is not None else c._local_tz_name()
500
+
501
+ def resolve(token: str) -> AnalyticsWindow:
502
+ match = c._DIFF_NW_AGO_RE.match(token)
503
+ if token in {"this-week", "last-week"} or match:
504
+ local_now = now.astimezone(tz) if tz is not None else now.astimezone()
505
+ current_start, _ = compute_week_bounds(
506
+ local_now, get_week_start_name(config),
507
+ )
508
+ start = dt.datetime.combine(current_start, dt.time.min, tzinfo=local_now.tzinfo)
509
+ if token == "this-week":
510
+ end = now
511
+ else:
512
+ weeks_back = 1 if token == "last-week" else int(match.group(1))
513
+ start -= dt.timedelta(days=7 * weeks_back)
514
+ end = start + dt.timedelta(days=7)
515
+ return AnalyticsWindow(token, "week", start, end)
516
+ parsed = c._parse_diff_window(
517
+ token, now_utc=now, anchor_resets_at=None, anchor_week_start=None,
518
+ tz_name=tz_name,
519
+ )
520
+ return AnalyticsWindow(parsed.label, parsed.kind, parsed.start_utc, parsed.end_utc)
521
+
522
+ args.window_a, args.window_b = resolve(args.a), resolve(args.b)
523
+ return args.window_a, args.window_b
524
+
525
+
526
+ def _resolve_source_report_as_of(args: object) -> dt.datetime:
527
+ as_of = getattr(args, "as_of", None)
528
+ if isinstance(as_of, dt.datetime):
529
+ return as_of
530
+ args.as_of = _command_as_of().astimezone(UTC)
531
+ return args.as_of
532
+
533
+
534
+ def _source_entries(
535
+ args: object, start: dt.datetime, end: dt.datetime, *, qualified: bool,
536
+ inclusive_end: bool = False, group: str = "git-root", sync: bool | None = None,
537
+ force_direct: bool = False,
538
+ ) -> tuple[QualifiedCodexEntry, ...]:
539
+ provided = getattr(args, "source_entries", None)
540
+ if provided is not None:
541
+ values = tuple(provided)
542
+ return assign_collision_safe_project_labels(values) if qualified else values
543
+ speed = _cctally()._resolve_codex_speed(str(getattr(args, "speed", "auto")))
544
+ if sync is None:
545
+ sync = bool(getattr(args, "_source_analytics_sync", not bool(getattr(args, "offline", False))))
546
+ query_end = end + dt.timedelta(microseconds=1) if inclusive_end else end
547
+ if qualified:
548
+ values = load_qualified_codex_entries(
549
+ start, query_end, speed=speed, sync=sync, group=group,
550
+ )
551
+ return assign_collision_safe_project_labels(values)
552
+ return load_codex_accounting_entries(
553
+ start, query_end, speed=speed, sync=sync, force_direct=force_direct,
554
+ )
555
+
556
+
557
+ def _project_selectors(value: object) -> tuple[str, ...]:
558
+ if value is None:
559
+ return ()
560
+ raw = value if isinstance(value, (list, tuple)) else (value,)
561
+ selectors: list[str] = []
562
+ for item in raw:
563
+ if not isinstance(item, str) or not item:
564
+ raise SourceUsageError("--project must be a non-empty project key or display label")
565
+ selectors.append(item)
566
+ return tuple(selectors)
567
+
568
+
569
+ def validate_source_project_selectors(value: object) -> None:
570
+ """Reject malformed opaque keys before a source cache read occurs."""
571
+ for selector in _project_selectors(value):
572
+ if selector.startswith("project:") and not _OPAQUE_PROJECT_KEY_RE.fullmatch(selector):
573
+ raise SourceUsageError(
574
+ "invalid opaque project key; expected project:<24 lowercase hex characters>"
575
+ )
576
+
577
+
578
+ def _filter_project_entries(
579
+ entries: Iterable[QualifiedCodexEntry], value: object,
580
+ ) -> tuple[QualifiedCodexEntry, ...]:
581
+ """Apply exact opaque-key or unique display-label project selectors."""
582
+ selectors = _project_selectors(value)
583
+ values = tuple(entries)
584
+ if not selectors:
585
+ return values
586
+ validate_source_project_selectors(selectors)
587
+ by_emitted_label: dict[str, set[str]] = defaultdict(set)
588
+ by_raw_label: dict[str, set[str]] = defaultdict(set)
589
+ keys = {entry.project_key for entry in values}
590
+ for entry in values:
591
+ by_emitted_label[emitted_project_label(entry)].add(entry.project_key)
592
+ by_raw_label[entry.project_label].add(entry.project_key)
593
+ selected: set[str] = set()
594
+ for selector in selectors:
595
+ if selector.startswith("project:"):
596
+ if selector in keys:
597
+ selected.add(selector)
598
+ continue
599
+ # Exact emitted collision-safe labels win over raw labels so each
600
+ # label JSON, terminal, and share expose round-trips as a selector.
601
+ matches = by_emitted_label.get(selector)
602
+ if matches is None:
603
+ matches = by_raw_label.get(selector, set())
604
+ if len(matches) > 1:
605
+ raise SourceUsageError(
606
+ f"--project display label {selector!r} is ambiguous; use an exact projectKey"
607
+ )
608
+ selected.update(matches)
609
+ return tuple(entry for entry in values if entry.project_key in selected)
610
+
611
+
612
+ def _filter_model_entries(
613
+ entries: Iterable[QualifiedCodexEntry], value: object,
614
+ ) -> tuple[QualifiedCodexEntry, ...]:
615
+ patterns = value if isinstance(value, (list, tuple)) else (() if value is None else (value,))
616
+ normalized = tuple(str(pattern).lower() for pattern in patterns if str(pattern))
617
+ if not normalized:
618
+ return tuple(entries)
619
+ return tuple(
620
+ entry for entry in entries
621
+ if any(pattern in entry.model.lower() for pattern in normalized)
622
+ )
623
+
624
+
625
+ _CLAUDE_DIFF_SECTIONS = frozenset({"overall", "models", "projects", "cache"})
626
+ _CODEX_DIFF_SECTIONS = frozenset({"overall", "models", "projects", "token-reuse"})
627
+
628
+
629
+ def _diff_only_for_provider(value: object, provider: str) -> str | None:
630
+ """Keep each all-source diff leg inside its own section vocabulary."""
631
+ if value is None:
632
+ return None
633
+ selected = [item.strip() for item in str(value).split(",") if item.strip()]
634
+ supported = _CLAUDE_DIFF_SECTIONS if provider == "claude" else _CODEX_DIFF_SECTIONS
635
+ return ",".join(item for item in selected if item in supported)
636
+
637
+
638
+ def _validate_source_diff_controls(
639
+ args: object, window_a: AnalyticsWindow, window_b: AnalyticsWindow,
640
+ ) -> str:
641
+ """Reject invalid source-aware diff controls before either provider starts."""
642
+ source = getattr(args, "source", "codex")
643
+ selected = [item.strip() for item in str(getattr(args, "only", "")).split(",") if item.strip()]
644
+ if getattr(args, "only", None) is not None:
645
+ if not selected:
646
+ raise SourceUsageError("diff: --only specified no sections")
647
+ supported = _CODEX_DIFF_SECTIONS if source == "codex" else (_CLAUDE_DIFF_SECTIONS | _CODEX_DIFF_SECTIONS)
648
+ unknown = [item for item in selected if item not in supported]
649
+ if unknown:
650
+ raise SourceUsageError(
651
+ "diff: --only contains unknown section(s): " + ", ".join(unknown)
652
+ )
653
+ for extra in (item.strip() for item in str(getattr(args, "with_extra", "") or "").split(",")):
654
+ if extra in {"trend", "time"}:
655
+ raise RuntimeError(
656
+ f"diff: --with {extra} is not yet implemented (deferred to v1.1)"
657
+ )
658
+ return resolve_codex_diff_normalization(
659
+ window_a, window_b, allow_mismatch=bool(getattr(args, "allow_mismatch", False)),
660
+ )
661
+
662
+
663
+ def _iso_z(value: dt.datetime) -> str:
664
+ return value.astimezone(UTC).isoformat().replace("+00:00", "Z")
665
+
666
+
667
+ _CLAUDE_COMMANDS = {
668
+ "project": "_cmd_project_claude",
669
+ "diff": "_cmd_diff_claude",
670
+ "range-cost": "_cmd_range_cost_claude",
671
+ "cache-report": "_cmd_cache_report_claude",
672
+ "report": "_cmd_report_claude",
673
+ }
674
+
675
+
676
+ def _claude_result_status(command: str, payload: dict[str, object]) -> str:
677
+ """Classify an established Claude JSON payload without reshaping it."""
678
+ if command == "project":
679
+ return "ok" if payload.get("projects") else "empty"
680
+ if command == "range-cost":
681
+ return "ok" if int(payload.get("matchedEntries", 0)) else "empty"
682
+ if command == "cache-report":
683
+ rows = payload.get("sessions", payload.get("days", ()))
684
+ return "ok" if rows else "empty"
685
+ if command == "report":
686
+ return "ok" if payload.get("trend") else "empty"
687
+ if command == "diff":
688
+ for section in payload.get("sections", ()):
689
+ if isinstance(section, dict) and section.get("name") == "overall":
690
+ rows = section.get("rows", ())
691
+ if not rows:
692
+ return "empty"
693
+ row = rows[0] if isinstance(rows, list) else None
694
+ if not isinstance(row, dict):
695
+ return "empty"
696
+ for side in (row.get("a"), row.get("b")):
697
+ if isinstance(side, dict) and any(
698
+ side.get(name, 0) for name in (
699
+ "cost_usd", "tokens_input", "tokens_output",
700
+ "tokens_cache_read", "tokens_cache_write",
701
+ )
702
+ ):
703
+ return "ok"
704
+ return "empty"
705
+ return "empty"
706
+ raise ValueError(f"unknown Claude analytics command: {command}")
707
+
708
+
709
+ def _run_claude_json_adapter(args: object, command: str) -> SourceResult[dict[str, object] | None]:
710
+ """Invoke an established Claude handler through its structured result seam.
711
+
712
+ The handlers retain their ordinary rendering paths. This adapter uses the
713
+ opt-in sink solely for the all-source composition path, so it never captures
714
+ or reparses terminal output and the provider block remains exact legacy JSON.
715
+ """
716
+ c = _cctally()
717
+ handler_name = _CLAUDE_COMMANDS[command]
718
+ handler = getattr(c, handler_name)
719
+ claude_args = copy(args)
720
+ claude_args.source = "claude"
721
+ claude_args.format = None
722
+ claude_args.json = True
723
+ # This is an internal structured-result invocation, not a destination
724
+ # request. Clear the public share flags before the legacy handler's
725
+ # defense-in-depth validation so it observes a normal JSON invocation.
726
+ claude_args.output = None
727
+ claude_args.copy = False
728
+ claude_args.open_after_write = False
729
+ # The source-aware parser owns ``--format`` but not every legacy share
730
+ # presentation default. Give the structured invocation the same default
731
+ # namespace that an ordinary legacy JSON parse would have.
732
+ for name, value in (
733
+ ("theme", "light"),
734
+ ("no_branding", False),
735
+ ("reveal_projects", False),
736
+ ):
737
+ if not hasattr(claude_args, name):
738
+ setattr(claude_args, name, value)
739
+ if command == "range-cost":
740
+ # The shared Codex resolver stores parsed bounds on the original
741
+ # namespace; the established Claude handler intentionally owns its
742
+ # own ISO parsing, so hand it back the exact wire spelling.
743
+ if isinstance(getattr(claude_args, "start", None), dt.datetime):
744
+ claude_args.start = _iso_z(claude_args.start)
745
+ if isinstance(getattr(claude_args, "end", None), dt.datetime):
746
+ claude_args.end = _iso_z(claude_args.end)
747
+ # The all-source adapter needs the complete legacy JSON object to
748
+ # compose compatible physical cost. The public combined path renders
749
+ # its total-only scalar after both provider legs have been read.
750
+ claude_args.total_only = False
751
+ if command == "diff":
752
+ claude_args.emit_json = True
753
+ captured: list[dict[str, object]] = []
754
+ claude_args._source_result_sink = captured.append
755
+ exit_code = handler(claude_args)
756
+ if exit_code != 0 or len(captured) != 1:
757
+ return SourceResult("claude", "unavailable", None)
758
+ payload = captured[0]
759
+ return SourceResult("claude", _claude_result_status(command, payload), payload)
760
+
761
+
762
+ def _source_block_wire(result: SourceResult, *, diff: bool) -> dict[str, object]:
763
+ """Keep a provider block's native JSON exactly intact in an all result."""
764
+ if result.source == "claude":
765
+ return {
766
+ "source": "claude",
767
+ "status": result.status,
768
+ "data": result.data,
769
+ "warnings": [
770
+ {"code": warning.code, "message": warning.message}
771
+ for warning in result.warnings
772
+ ],
773
+ }
774
+ direct = source_result_wire(result, diff=diff)
775
+ return {
776
+ "source": direct["source"],
777
+ "status": direct["status"],
778
+ "data": direct["data"],
779
+ "warnings": direct["warnings"],
780
+ }
781
+
782
+
783
+ def _legacy_claude_totals(payload: object) -> tuple[float, int]:
784
+ """Read compatible accounting once from an exact legacy JSON payload."""
785
+ if not isinstance(payload, dict):
786
+ return 0.0, 0
787
+ totals = payload.get("totals")
788
+ if isinstance(totals, dict):
789
+ cost = totals.get("costUsd", totals.get("cost", 0.0))
790
+ tokens = totals.get("totalTokens", 0)
791
+ if "totalTokens" not in totals and isinstance(payload.get("projects"), list):
792
+ tokens = sum(
793
+ int(row.get("inputTokens", 0)) + int(row.get("outputTokens", 0))
794
+ + int(row.get("cacheWriteTokens", 0)) + int(row.get("cacheReadTokens", 0))
795
+ for row in payload["projects"] if isinstance(row, dict)
796
+ )
797
+ return float(cost), int(tokens)
798
+ if "totalCostUSD" in payload:
799
+ return (
800
+ float(payload.get("totalCostUSD", 0.0)),
801
+ sum(int(row.get("totalTokens", 0)) for row in payload.get("modelBreakdowns", ()) if isinstance(row, dict)),
802
+ )
803
+ return 0.0, 0
804
+
805
+
806
+ def _codex_totals(result: SourceResult) -> tuple[float, int]:
807
+ """Read one compatible physical Codex total from its native wire shape."""
808
+ direct = source_result_wire(result)
809
+ data = direct.get("data")
810
+ if not isinstance(data, dict):
811
+ return 0.0, 0
812
+ totals = data.get("totals")
813
+ if not isinstance(totals, dict):
814
+ return 0.0, 0
815
+ return float(totals.get("costUsd", 0.0)), int(totals.get("totalTokens", 0))
816
+
817
+
818
+ def _legacy_diff_totals(payload: object) -> tuple[tuple[float, int], tuple[float, int]]:
819
+ """Extract the two physical Claude totals from the frozen diff payload."""
820
+ if not isinstance(payload, dict):
821
+ return (0.0, 0), (0.0, 0)
822
+ for section in payload.get("sections", ()):
823
+ if not isinstance(section, dict) or section.get("name") != "overall":
824
+ continue
825
+ rows = section.get("rows")
826
+ if not isinstance(rows, list) or not rows or not isinstance(rows[0], dict):
827
+ break
828
+ row = rows[0]
829
+
830
+ def side(name: str) -> tuple[float, int]:
831
+ values = row.get(name)
832
+ if not isinstance(values, dict):
833
+ return 0.0, 0
834
+ return (
835
+ float(values.get("cost_usd", 0.0)),
836
+ sum(int(values.get(token, 0)) for token in (
837
+ "tokens_input", "tokens_output", "tokens_cache_read", "tokens_cache_write",
838
+ )),
839
+ )
840
+
841
+ return side("a"), side("b")
842
+ return (0.0, 0), (0.0, 0)
843
+
844
+
845
+ def _codex_diff_totals(result: SourceResult) -> tuple[tuple[float, int], tuple[float, int]]:
846
+ direct = source_result_wire(result, diff=True)
847
+ data = direct.get("data")
848
+ combined = data.get("combined") if isinstance(data, dict) else None
849
+ if not isinstance(combined, dict):
850
+ return (0.0, 0), (0.0, 0)
851
+
852
+ def side(name: str) -> tuple[float, int]:
853
+ costs = combined.get("cost_usd")
854
+ tokens = combined.get("total_tokens")
855
+ if not isinstance(costs, dict) or not isinstance(tokens, dict):
856
+ return 0.0, 0
857
+ return float(costs.get(name, 0.0)), int(tokens.get(name, 0))
858
+
859
+ return side("a"), side("b")
860
+
861
+
862
+ def _all_source_wire(
863
+ claude: SourceResult, codex: SourceResult, *, diff: bool, report: bool,
864
+ ) -> dict[str, object]:
865
+ """Compose exact provider payloads and only compatible physical totals."""
866
+ sources = [
867
+ _source_block_wire(claude, diff=diff),
868
+ _source_block_wire(codex, diff=diff),
869
+ ]
870
+ if diff:
871
+ result: dict[str, object] = {"schema_version": 1, "source": "all"}
872
+ if not report:
873
+ (ca, ta), (cb, tb) = _legacy_diff_totals(claude.data)
874
+ (xa, xa_tokens), (xb, xb_tokens) = _codex_diff_totals(codex)
875
+ result["combined"] = {
876
+ "cost_usd": {"a": ca + xa, "b": cb + xb, "delta": (cb + xb) - (ca + xa)},
877
+ "total_tokens": {"a": ta + xa_tokens, "b": tb + xb_tokens, "delta": (tb + xb_tokens) - (ta + xa_tokens)},
878
+ }
879
+ result["sources"] = sources
880
+ result["warnings"] = []
881
+ return result
882
+ result = {"schemaVersion": 1, "source": "all"}
883
+ if not report:
884
+ claude_cost, claude_tokens = _legacy_claude_totals(claude.data)
885
+ codex_cost, codex_tokens = _codex_totals(codex)
886
+ result["combined"] = {
887
+ "costUsd": claude_cost + codex_cost,
888
+ "totalTokens": claude_tokens + codex_tokens,
889
+ }
890
+ result["sources"] = sources
891
+ result["warnings"] = []
892
+ return result
893
+
894
+
895
+ _TERMINAL_TITLES = {
896
+ "project": "Project Report",
897
+ "diff": "Diff Report",
898
+ "range-cost": "Range Cost Report",
899
+ "cache-report": "Token Reuse Report",
900
+ "report": "Dollars-per-Percent Report",
901
+ }
902
+
903
+
904
+ def _share_datetime(value: object, fallback: dt.datetime) -> dt.datetime:
905
+ """Return an aware UTC instant without accepting untrusted display strings."""
906
+ if isinstance(value, dt.datetime):
907
+ return value.astimezone(UTC)
908
+ if isinstance(value, str):
909
+ try:
910
+ parsed = dt.datetime.fromisoformat(value.replace("Z", "+00:00"))
911
+ except ValueError:
912
+ return fallback
913
+ if parsed.tzinfo is not None and parsed.utcoffset() is not None:
914
+ return parsed.astimezone(UTC)
915
+ return fallback
916
+
917
+
918
+ def _source_share_period(command: str, result: SourceResult) -> tuple[dt.datetime, dt.datetime]:
919
+ """Derive a deterministic bounded share period from public result metadata."""
920
+ fallback = _command_as_of().astimezone(UTC)
921
+ data = result.data
922
+ start = getattr(data, "range_start", getattr(data, "start", None))
923
+ end = getattr(data, "range_end", getattr(data, "end", None))
924
+ if command == "report":
925
+ end = getattr(data, "as_of", end)
926
+ if command == "diff":
927
+ windows = getattr(data, "windows", ())
928
+ if len(windows) == 2:
929
+ start = min(window.window.start_at for window in windows)
930
+ end = max(window.window.end_at for window in windows)
931
+ if isinstance(data, dict):
932
+ start = data.get("rangeStart", data.get("rangeStartIso", data.get("start", start)))
933
+ end = data.get("rangeEnd", data.get("rangeEndIso", data.get("end", end)))
934
+ if command == "diff":
935
+ windows = data.get("windows")
936
+ if isinstance(windows, dict):
937
+ values = tuple(value for value in windows.values() if isinstance(value, dict))
938
+ starts = tuple(value.get("start_at") for value in values)
939
+ ends = tuple(value.get("end_at") for value in values)
940
+ parsed_starts = tuple(_share_datetime(value, fallback) for value in starts)
941
+ parsed_ends = tuple(_share_datetime(value, fallback) for value in ends)
942
+ if parsed_starts:
943
+ start = min(parsed_starts)
944
+ if parsed_ends:
945
+ end = max(parsed_ends)
946
+ if command == "report" and not start:
947
+ trend = data.get("trend")
948
+ if isinstance(trend, list) and trend and isinstance(trend[0], dict):
949
+ start = trend[0].get("weekStartDate", start)
950
+ last = trend[-1] if isinstance(trend[-1], dict) else trend[0]
951
+ end = last.get("weekStartDate", end)
952
+ end_at = _share_datetime(end, fallback)
953
+ start_at = _share_datetime(start, end_at)
954
+ return (start_at, end_at if end_at >= start_at else start_at)
955
+
956
+
957
+ def _share_float(value: object, default: float = 0.0) -> float:
958
+ """Read a legacy JSON number without letting malformed optional fields fail share."""
959
+ try:
960
+ return float(value)
961
+ except (TypeError, ValueError):
962
+ return default
963
+
964
+
965
+ def _share_int(value: object, default: int = 0) -> int:
966
+ """Read a legacy JSON integer without treating bools as token counts."""
967
+ if isinstance(value, bool):
968
+ return default
969
+ try:
970
+ return int(value)
971
+ except (TypeError, ValueError):
972
+ return default
973
+
974
+
975
+ def _claude_project_share_rows(data: dict[str, object], lib) -> tuple[tuple, tuple, tuple]:
976
+ """Adapt the established ``project`` JSON payload without Codex field guesses."""
977
+ columns = (
978
+ lib.ColumnSpec(key="project", label="Project", kind="project"),
979
+ lib.ColumnSpec(key="cost", label="$ Cost", align="right"),
980
+ lib.ColumnSpec(key="tokens", label="Tokens", align="right"),
981
+ )
982
+ rows = []
983
+ for payload_row in data.get("projects", ()):
984
+ if not isinstance(payload_row, dict):
985
+ continue
986
+ cost = _share_float(payload_row.get("costUsd"))
987
+ token_count = sum(_share_int(payload_row.get(name)) for name in (
988
+ "inputTokens", "outputTokens", "cacheWriteTokens", "cacheReadTokens",
989
+ ))
990
+ rows.append(lib.Row(cells={
991
+ "project": lib.ProjectCell(
992
+ str(payload_row.get("displayKey", "(unknown)")), rank_cost=cost,
993
+ ),
994
+ "cost": lib.MoneyCell(cost),
995
+ "tokens": lib.TextCell(f"{token_count:,}"),
996
+ }))
997
+ total_cost, total_tokens = _legacy_claude_totals(data)
998
+ totals = (
999
+ lib.Totalled(label="Total", value=f"${total_cost:,.2f}"),
1000
+ lib.Totalled(label="Tokens", value=f"{total_tokens:,}"),
1001
+ )
1002
+ return columns, tuple(rows), totals
1003
+
1004
+
1005
+ def _diff_metric_tokens(metric: object) -> int | None:
1006
+ """Read the source-native total-token field or its frozen Claude inputs."""
1007
+ if not isinstance(metric, dict):
1008
+ return None
1009
+ if "total_tokens" in metric:
1010
+ return _share_int(metric.get("total_tokens"))
1011
+ token_keys = (
1012
+ "tokens_input", "tokens_output", "tokens_cache_read", "tokens_cache_write",
1013
+ )
1014
+ if not any(key in metric for key in token_keys):
1015
+ return None
1016
+ return sum(_share_int(metric.get(key)) for key in token_keys)
1017
+
1018
+
1019
+ def _diff_sections(data: dict[str, object]):
1020
+ """Yield legacy and Codex diff sections without flattening provider shape."""
1021
+ for section in data.get("sections", ()):
1022
+ if not isinstance(section, dict):
1023
+ continue
1024
+ key = str(section.get("key", section.get("name", "section")))
1025
+ label = str(section.get("label", key.replace("-", " ").title()))
1026
+ section_data = section.get("data")
1027
+ rows = (
1028
+ section_data.get("rows", ())
1029
+ if isinstance(section_data, dict) else section.get("rows", ())
1030
+ )
1031
+ yield key, label, str(section.get("status", "ok")), rows
1032
+
1033
+
1034
+ def _diff_share_rows(data: dict[str, object], lib) -> tuple[tuple, tuple, tuple]:
1035
+ """Adapt one native diff payload with row identities and full comparison metrics."""
1036
+ columns = (
1037
+ lib.ColumnSpec(key="section", label="Section"),
1038
+ lib.ColumnSpec(key="item", label="Item"),
1039
+ lib.ColumnSpec(key="status", label="Status"),
1040
+ lib.ColumnSpec(key="a_cost", label="A $ Cost", align="right"),
1041
+ lib.ColumnSpec(key="b_cost", label="B $ Cost", align="right"),
1042
+ lib.ColumnSpec(key="delta_cost", label="Δ $ Cost", align="right"),
1043
+ lib.ColumnSpec(key="a_tokens", label="A Tokens", align="right"),
1044
+ lib.ColumnSpec(key="b_tokens", label="B Tokens", align="right"),
1045
+ lib.ColumnSpec(key="delta_tokens", label="Δ Tokens", align="right"),
1046
+ )
1047
+ rows = []
1048
+ for section_key, section_label, section_status, payload_rows in _diff_sections(data):
1049
+ for payload_row in payload_rows:
1050
+ if not isinstance(payload_row, dict):
1051
+ continue
1052
+ side_a = payload_row.get("a") if isinstance(payload_row.get("a"), dict) else {}
1053
+ side_b = payload_row.get("b") if isinstance(payload_row.get("b"), dict) else {}
1054
+ delta = payload_row.get("delta") if isinstance(payload_row.get("delta"), dict) else {}
1055
+ label = str(payload_row.get("label", section_label))
1056
+ status = str(payload_row.get("status", section_status))
1057
+ if section_key == "projects":
1058
+ item = lib.ProjectCell(
1059
+ label,
1060
+ rank_cost=_share_float(side_b.get("cost_usd")),
1061
+ identity=str(payload_row.get("key", payload_row.get("projectKey", label))),
1062
+ )
1063
+ else:
1064
+ item = lib.TextCell(label)
1065
+ a_tokens = _diff_metric_tokens(side_a)
1066
+ b_tokens = _diff_metric_tokens(side_b)
1067
+ delta_tokens = _diff_metric_tokens(delta)
1068
+ rows.append(lib.Row(cells={
1069
+ "section": lib.TextCell(section_label),
1070
+ "item": item,
1071
+ "status": lib.TextCell(status),
1072
+ "a_cost": lib.MoneyCell(_share_float(side_a.get("cost_usd"))),
1073
+ "b_cost": lib.MoneyCell(_share_float(side_b.get("cost_usd"))),
1074
+ "delta_cost": lib.DeltaCell(_share_float(delta.get("cost_usd")), "$"),
1075
+ "a_tokens": lib.TextCell("—" if a_tokens is None else f"{a_tokens:,}"),
1076
+ "b_tokens": lib.TextCell("—" if b_tokens is None else f"{b_tokens:,}"),
1077
+ "delta_tokens": lib.TextCell(
1078
+ "—" if delta_tokens is None else f"{delta_tokens:+,}"
1079
+ ),
1080
+ }))
1081
+ if isinstance(data.get("combined"), dict):
1082
+ combined = data["combined"]
1083
+ costs = combined.get("cost_usd") if isinstance(combined.get("cost_usd"), dict) else {}
1084
+ tokens = combined.get("total_tokens") if isinstance(combined.get("total_tokens"), dict) else {}
1085
+ cost_a, cost_b = _share_float(costs.get("a")), _share_float(costs.get("b"))
1086
+ tokens_a, tokens_b = _share_int(tokens.get("a")), _share_int(tokens.get("b"))
1087
+ else:
1088
+ (cost_a, tokens_a), (cost_b, tokens_b) = _legacy_diff_totals(data)
1089
+ delta_cost = cost_b - cost_a
1090
+ totals = (
1091
+ lib.Totalled(label="A total", value=f"${cost_a:,.2f}"),
1092
+ lib.Totalled(label="B total", value=f"${cost_b:,.2f}"),
1093
+ lib.Totalled(
1094
+ label="Δ total",
1095
+ value=f"{'+' if delta_cost >= 0 else '-'}${abs(delta_cost):,.2f}",
1096
+ ),
1097
+ lib.Totalled(label="A tokens", value=f"{tokens_a:,}"),
1098
+ lib.Totalled(label="B tokens", value=f"{tokens_b:,}"),
1099
+ lib.Totalled(label="Δ tokens", value=f"{(tokens_b - tokens_a):+,}"),
1100
+ )
1101
+ return columns, tuple(rows), totals
1102
+
1103
+
1104
+ def _claude_diff_share_rows(data: dict[str, object], lib) -> tuple[tuple, tuple, tuple]:
1105
+ """Adapt frozen legacy diff sections with their A/B/delta quantities intact."""
1106
+ return _diff_share_rows(data, lib)
1107
+
1108
+
1109
+ def _claude_range_cost_share_rows(data: dict[str, object], lib) -> tuple[tuple, tuple, tuple]:
1110
+ """Adapt legacy ``modelBreakdowns`` and ``totalCostUSD`` exactly once."""
1111
+ columns = (
1112
+ lib.ColumnSpec(key="model", label="Model"),
1113
+ lib.ColumnSpec(key="tokens", label="Tokens", align="right"),
1114
+ lib.ColumnSpec(key="cost", label="$ Cost", align="right"),
1115
+ )
1116
+ rows = []
1117
+ for payload_row in data.get("modelBreakdowns", ()):
1118
+ if not isinstance(payload_row, dict):
1119
+ continue
1120
+ rows.append(lib.Row(cells={
1121
+ "model": lib.TextCell(str(payload_row.get("model", "(unknown)"))),
1122
+ "tokens": lib.TextCell(f"{_share_int(payload_row.get('totalTokens')):,}"),
1123
+ "cost": lib.MoneyCell(_share_float(payload_row.get("costUSD"))),
1124
+ }))
1125
+ if not rows and _share_int(data.get("matchedEntries")):
1126
+ cost, tokens = _legacy_claude_totals(data)
1127
+ rows.append(lib.Row(cells={
1128
+ "model": lib.TextCell("Total"),
1129
+ "tokens": lib.TextCell(f"{tokens:,}"),
1130
+ "cost": lib.MoneyCell(cost),
1131
+ }))
1132
+ cost, tokens = _legacy_claude_totals(data)
1133
+ totals = (
1134
+ lib.Totalled(label="Total", value=f"${cost:,.2f}"),
1135
+ lib.Totalled(label="Tokens", value=f"{tokens:,}"),
1136
+ )
1137
+ return columns, tuple(rows), totals
1138
+
1139
+
1140
+ def _claude_cache_report_share_rows(data: dict[str, object], lib) -> tuple[tuple, tuple, tuple]:
1141
+ """Adapt legacy cache-day/session rows without exposing session/path fields."""
1142
+ columns = (
1143
+ lib.ColumnSpec(key="label", label="Group"),
1144
+ lib.ColumnSpec(key="reuse", label="Cached Input", align="right"),
1145
+ lib.ColumnSpec(key="cost", label="$ Cost", align="right"),
1146
+ )
1147
+ payload_rows = data.get("days")
1148
+ if not isinstance(payload_rows, (list, tuple)):
1149
+ payload_rows = data.get("sessions")
1150
+ rows = []
1151
+ for index, payload_row in enumerate(payload_rows if isinstance(payload_rows, (list, tuple)) else ()):
1152
+ if not isinstance(payload_row, dict):
1153
+ continue
1154
+ # Session ids and paths are intentionally never suitable share labels.
1155
+ label = payload_row.get("date") or f"Session {index + 1}"
1156
+ cached_percent = payload_row.get("cacheHitPercent")
1157
+ rows.append(lib.Row(cells={
1158
+ "label": lib.TextCell(str(label)),
1159
+ "reuse": lib.TextCell(
1160
+ "—" if cached_percent is None else f"{_share_float(cached_percent):.1f}%"
1161
+ ),
1162
+ "cost": lib.MoneyCell(_share_float(payload_row.get("cost"))),
1163
+ }))
1164
+ total_cost, total_tokens = _legacy_claude_totals(data)
1165
+ totals = (
1166
+ lib.Totalled(label="Total", value=f"${total_cost:,.2f}"),
1167
+ lib.Totalled(label="Tokens", value=f"{total_tokens:,}"),
1168
+ )
1169
+ return columns, tuple(rows), totals
1170
+
1171
+
1172
+ def _claude_report_share_rows(data: dict[str, object], lib) -> tuple[tuple, tuple, tuple]:
1173
+ """Adapt the established weekly trend rather than inventing quota-series rows."""
1174
+ columns = (
1175
+ lib.ColumnSpec(key="week", label="Week"),
1176
+ lib.ColumnSpec(key="used", label="% Used", align="right"),
1177
+ lib.ColumnSpec(key="cost", label="$ Cost", align="right"),
1178
+ lib.ColumnSpec(key="per_percent", label="$/1%", align="right"),
1179
+ )
1180
+ rows = []
1181
+ for payload_row in data.get("trend", ()):
1182
+ if not isinstance(payload_row, dict):
1183
+ continue
1184
+ date_value = str(payload_row.get("weekStartDate", "—"))
1185
+ try:
1186
+ display_week = dt.date.fromisoformat(date_value).strftime("%b %d")
1187
+ except ValueError:
1188
+ display_week = date_value
1189
+ used = payload_row.get("weeklyPercent")
1190
+ per_percent = payload_row.get("dollarsPerPercent")
1191
+ rows.append(lib.Row(cells={
1192
+ "week": lib.TextCell(display_week),
1193
+ "used": lib.TextCell("—" if used is None else f"{_share_float(used):.1f}%"),
1194
+ "cost": lib.MoneyCell(_share_float(payload_row.get("weeklyCostUSD"))),
1195
+ "per_percent": lib.TextCell(
1196
+ "—" if per_percent is None else f"${_share_float(per_percent):.2f}"
1197
+ ),
1198
+ }))
1199
+ total_cost = sum(
1200
+ _share_float(row.get("weeklyCostUSD")) for row in data.get("trend", ())
1201
+ if isinstance(row, dict)
1202
+ )
1203
+ return columns, tuple(rows), (lib.Totalled(label="Trend total", value=f"${total_cost:,.2f}"),)
1204
+
1205
+
1206
+ def _claude_legacy_share_rows(command: str, data: dict[str, object], lib) -> tuple[tuple, tuple, tuple]:
1207
+ """Route each legacy payload through its explicit, source-native adapter."""
1208
+ adapters = {
1209
+ "project": _claude_project_share_rows,
1210
+ "diff": _claude_diff_share_rows,
1211
+ "range-cost": _claude_range_cost_share_rows,
1212
+ "cache-report": _claude_cache_report_share_rows,
1213
+ "report": _claude_report_share_rows,
1214
+ }
1215
+ return adapters[command](data, lib)
1216
+
1217
+
1218
+ def _source_share_rows(command: str, result: SourceResult, lib) -> tuple[tuple, tuple, tuple]:
1219
+ """Build a compact, source-safe table from the public provider result shape."""
1220
+ if result.status == "unavailable" or result.data is None:
1221
+ return (), (), ()
1222
+ if result.source == "codex":
1223
+ wire = source_result_wire(result, diff=(command == "diff"))
1224
+ data = wire.get("data")
1225
+ else:
1226
+ data = result.data
1227
+ if not isinstance(data, dict):
1228
+ return (), (), ()
1229
+
1230
+ if command == "project":
1231
+ columns = (
1232
+ lib.ColumnSpec(key="project", label="Project", kind="project"),
1233
+ lib.ColumnSpec(key="cost", label="$ Cost", align="right"),
1234
+ lib.ColumnSpec(key="tokens", label="Tokens", align="right"),
1235
+ )
1236
+ rows = []
1237
+ for row in data.get("projects", ()):
1238
+ if not isinstance(row, dict):
1239
+ continue
1240
+ label = str(row.get("displayLabel", row.get("project", "(unknown)")))
1241
+ identity = row.get("projectKey") if result.source == "codex" else None
1242
+ tokens = row.get("tokens", {})
1243
+ token_count = tokens.get("totalTokens", 0) if isinstance(tokens, dict) else 0
1244
+ rows.append(lib.Row(cells={
1245
+ "project": lib.ProjectCell(label, rank_cost=float(row.get("costUsd", 0.0)), identity=identity),
1246
+ "cost": lib.MoneyCell(float(row.get("costUsd", 0.0))),
1247
+ "tokens": lib.TextCell(f"{int(token_count):,}"),
1248
+ }))
1249
+ return columns, tuple(rows), ()
1250
+
1251
+ if command == "range-cost":
1252
+ columns = (
1253
+ lib.ColumnSpec(key="model", label="Model"),
1254
+ lib.ColumnSpec(key="tokens", label="Tokens", align="right"),
1255
+ lib.ColumnSpec(key="cost", label="$ Cost", align="right"),
1256
+ )
1257
+ model_rows = tuple(row for row in data.get("models", ()) if isinstance(row, dict))
1258
+ if model_rows:
1259
+ rows = tuple(
1260
+ lib.Row(cells={
1261
+ "model": lib.TextCell(str(row.get("model", "(unknown)"))),
1262
+ "tokens": lib.TextCell(f"{int(row.get('totalTokens', 0)):,}"),
1263
+ "cost": lib.MoneyCell(float(row.get("costUsd", 0.0))),
1264
+ })
1265
+ for row in model_rows
1266
+ )
1267
+ else:
1268
+ totals = data.get("totals") if isinstance(data.get("totals"), dict) else {}
1269
+ rows = (lib.Row(cells={
1270
+ "model": lib.TextCell("Total"),
1271
+ "tokens": lib.TextCell(f"{int(totals.get('totalTokens', 0)):,}"),
1272
+ "cost": lib.MoneyCell(float(totals.get("costUsd", 0.0))),
1273
+ }),)
1274
+ return columns, rows, ()
1275
+
1276
+ if command == "cache-report":
1277
+ columns = (
1278
+ lib.ColumnSpec(key="label", label="Group"),
1279
+ lib.ColumnSpec(key="reuse", label="Cached Input", align="right"),
1280
+ lib.ColumnSpec(key="cost", label="$ Cost", align="right"),
1281
+ )
1282
+ rows = []
1283
+ for section in data.get("sections", ()):
1284
+ if not isinstance(section, dict):
1285
+ continue
1286
+ for row in (section.get("data") or {}).get("rows", ()):
1287
+ if not isinstance(row, dict):
1288
+ continue
1289
+ rows.append(lib.Row(cells={
1290
+ "label": lib.TextCell(str(row.get("label", "Codex"))),
1291
+ "reuse": lib.TextCell(
1292
+ "—" if row.get("cachedInputPercent") is None
1293
+ else f"{float(row['cachedInputPercent']):.1f}%"
1294
+ ),
1295
+ "cost": lib.MoneyCell(float(row.get("costUsd", 0.0))),
1296
+ }))
1297
+ return columns, tuple(rows), ()
1298
+
1299
+ if command == "diff":
1300
+ return _diff_share_rows(data, lib)
1301
+
1302
+ columns = (
1303
+ lib.ColumnSpec(key="series", label="Quota Series"),
1304
+ lib.ColumnSpec(key="used", label="% Used", align="right"),
1305
+ lib.ColumnSpec(key="cost", label="$ Cost", align="right"),
1306
+ )
1307
+ rows = []
1308
+ for section in data.get("sections", ()):
1309
+ if not isinstance(section, dict):
1310
+ continue
1311
+ for series in (section.get("data") or {}).get("series", ()):
1312
+ if not isinstance(series, dict):
1313
+ continue
1314
+ for row in series.get("rows", ()):
1315
+ if not isinstance(row, dict):
1316
+ continue
1317
+ slot = str(series.get("slot", "quota"))
1318
+ window_minutes = series.get("windowMinutes")
1319
+ if isinstance(window_minutes, int) and window_minutes > 0:
1320
+ slot = f"{slot} · {window_minutes}m"
1321
+ rows.append(lib.Row(cells={
1322
+ "series": lib.TextCell(slot),
1323
+ "used": lib.TextCell("—" if row.get("usedPercent") is None else f"{float(row['usedPercent']):.1f}%"),
1324
+ "cost": lib.MoneyCell(float(row.get("costUsd", 0.0))),
1325
+ }))
1326
+ return columns, tuple(rows), ()
1327
+
1328
+
1329
+ def build_source_share_snapshot(
1330
+ command: str, source_result: SourceResult, *, reveal_projects: bool,
1331
+ ) -> "ShareSnapshot":
1332
+ """Return one source-bearing share snapshot without exposing local roots.
1333
+
1334
+ Callers compose two returned snapshots for an all-source artifact instead
1335
+ of inventing a synthetic ``source='all'`` snapshot.
1336
+ """
1337
+ if command not in _TERMINAL_TITLES:
1338
+ raise ValueError(f"unknown source share command: {command}")
1339
+ if source_result.source not in {"claude", "codex"}:
1340
+ raise ValueError("source share snapshots require Claude or Codex")
1341
+ c = _cctally()
1342
+ lib = c._share_load_lib()
1343
+ start, end = _source_share_period(command, source_result)
1344
+ if source_result.source == "claude" and isinstance(source_result.data, dict):
1345
+ columns, rows, totals = _claude_legacy_share_rows(command, source_result.data, lib)
1346
+ else:
1347
+ columns, rows, totals = _source_share_rows(command, source_result, lib)
1348
+ reason = (
1349
+ source_result.warnings[0].message
1350
+ if source_result.status == "unavailable" and source_result.warnings
1351
+ else "Source analytics are unavailable."
1352
+ )
1353
+ availability = "unavailable" if source_result.status == "unavailable" else (
1354
+ "empty" if source_result.status == "empty" else "ok"
1355
+ )
1356
+ source_label = "Claude" if source_result.source == "claude" else "Codex"
1357
+ period_label = f"{start.date().isoformat()} → {end.date().isoformat()} (UTC)"
1358
+ notes = tuple(warning.message for warning in source_result.warnings if source_result.status != "unavailable")
1359
+ return lib.ShareSnapshot(
1360
+ cmd=command,
1361
+ title=_TERMINAL_TITLES[command],
1362
+ subtitle=period_label,
1363
+ period=lib.PeriodSpec(start=start, end=end, display_tz="UTC", label=period_label),
1364
+ columns=columns,
1365
+ rows=rows,
1366
+ chart=None,
1367
+ totals=totals,
1368
+ notes=notes,
1369
+ generated_at=end,
1370
+ version=c._share_resolve_version(),
1371
+ source=source_result.source,
1372
+ source_label=source_label,
1373
+ availability=availability,
1374
+ availability_reason=reason if availability == "unavailable" else None,
1375
+ )
1376
+
1377
+
1378
+ def _terminal_amount(value: object) -> str:
1379
+ try:
1380
+ return f"${float(value):.6f}"
1381
+ except (TypeError, ValueError):
1382
+ return "$0.000000"
1383
+
1384
+
1385
+ def _terminal_tokens(value: object) -> str:
1386
+ try:
1387
+ return f"{int(value):,} tokens"
1388
+ except (TypeError, ValueError):
1389
+ return "0 tokens"
1390
+
1391
+
1392
+ def _terminal_delta_amount(value: object) -> str:
1393
+ try:
1394
+ amount = float(value)
1395
+ except (TypeError, ValueError):
1396
+ amount = 0.0
1397
+ return f"{'+' if amount >= 0 else '-'}${abs(amount):.6f}"
1398
+
1399
+
1400
+ def _terminal_delta_tokens(value: object) -> str:
1401
+ try:
1402
+ amount = int(value)
1403
+ except (TypeError, ValueError):
1404
+ amount = 0
1405
+ return f"{amount:+,} tokens"
1406
+
1407
+
1408
+ def _append_diff_terminal(lines: list[str], data: dict[str, object]) -> None:
1409
+ """Append source-native diff rows with comparison metrics and statuses."""
1410
+ combined = data.get("combined") if isinstance(data.get("combined"), dict) else {}
1411
+ costs = combined.get("cost_usd") if isinstance(combined.get("cost_usd"), dict) else {}
1412
+ tokens = combined.get("total_tokens") if isinstance(combined.get("total_tokens"), dict) else {}
1413
+ if costs or tokens:
1414
+ lines.append(
1415
+ "Overall: "
1416
+ f"A {_terminal_amount(costs.get('a'))} / {_terminal_tokens(tokens.get('a'))}; "
1417
+ f"B {_terminal_amount(costs.get('b'))} / {_terminal_tokens(tokens.get('b'))}; "
1418
+ f"Δ {_terminal_delta_amount(costs.get('delta'))} / "
1419
+ f"{_terminal_delta_tokens(tokens.get('delta'))}"
1420
+ )
1421
+ else:
1422
+ (cost_a, tokens_a), (cost_b, tokens_b) = _legacy_diff_totals(data)
1423
+ lines.append(
1424
+ "Overall: "
1425
+ f"A {_terminal_amount(cost_a)} / {_terminal_tokens(tokens_a)}; "
1426
+ f"B {_terminal_amount(cost_b)} / {_terminal_tokens(tokens_b)}; "
1427
+ f"Δ {_terminal_delta_amount(cost_b - cost_a)} / "
1428
+ f"{_terminal_delta_tokens(tokens_b - tokens_a)}"
1429
+ )
1430
+ for section_key, section_label, section_status, payload_rows in _diff_sections(data):
1431
+ lines.append(f"- {section_label}: {section_status}")
1432
+ for payload_row in payload_rows:
1433
+ if not isinstance(payload_row, dict):
1434
+ continue
1435
+ side_a = payload_row.get("a") if isinstance(payload_row.get("a"), dict) else {}
1436
+ side_b = payload_row.get("b") if isinstance(payload_row.get("b"), dict) else {}
1437
+ delta = payload_row.get("delta") if isinstance(payload_row.get("delta"), dict) else {}
1438
+ label = str(payload_row.get("label", section_key))
1439
+ status = str(payload_row.get("status", section_status))
1440
+ a_tokens = _diff_metric_tokens(side_a)
1441
+ b_tokens = _diff_metric_tokens(side_b)
1442
+ delta_tokens = _diff_metric_tokens(delta)
1443
+ lines.append(
1444
+ f" - {label} [{status}]: "
1445
+ f"A {_terminal_amount(side_a.get('cost_usd'))} / "
1446
+ f"{_terminal_tokens(a_tokens)}; "
1447
+ f"B {_terminal_amount(side_b.get('cost_usd'))} / "
1448
+ f"{_terminal_tokens(b_tokens)}; "
1449
+ f"Δ {_terminal_delta_amount(delta.get('cost_usd'))} / "
1450
+ f"{_terminal_delta_tokens(delta_tokens)}"
1451
+ )
1452
+
1453
+
1454
+ def _render_codex_terminal(
1455
+ command: str, result: SourceResult, *, diff: bool,
1456
+ include_unavailable_diagnostic: bool = True,
1457
+ ) -> str:
1458
+ """Render a compact, provider-native terminal report without raw paths."""
1459
+ title = _TERMINAL_TITLES.get(command, "Analytics Report")
1460
+ lines = [f"Codex {title}"]
1461
+ if result.status == "unavailable":
1462
+ if not include_unavailable_diagnostic:
1463
+ return "\n".join([*lines, "Unavailable."])
1464
+ diagnostic = result.warnings[0].message if result.warnings else "Codex analytics are unavailable."
1465
+ return "\n".join([*lines, f"Unavailable: {diagnostic}"])
1466
+ if result.status == "empty":
1467
+ return "\n".join([*lines, "No data."])
1468
+ if result.status == "partial":
1469
+ diagnostic = result.warnings[0].message if result.warnings else "Some Codex detail is unavailable."
1470
+ lines.append(f"Partial: {diagnostic}")
1471
+ wire = source_result_wire(result, diff=diff)
1472
+ data = wire.get("data")
1473
+ if not isinstance(data, dict):
1474
+ return "\n".join(lines)
1475
+ if command == "project":
1476
+ totals = data.get("totals", {})
1477
+ lines.append(
1478
+ f"Total: {_terminal_amount(totals.get('costUsd'))} · "
1479
+ f"{_terminal_tokens(totals.get('totalTokens'))}"
1480
+ )
1481
+ for row in data.get("projects", ()):
1482
+ if isinstance(row, dict):
1483
+ lines.append(
1484
+ f"- {row.get('displayLabel', '(unassigned)')}: "
1485
+ f"{_terminal_amount(row.get('costUsd'))} · "
1486
+ f"{_terminal_tokens(row.get('tokens', {}).get('totalTokens'))}"
1487
+ )
1488
+ # The pure project result omits this key unless --breakdown
1489
+ # was requested, so preserving it here makes the accepted
1490
+ # flag visible on both machine and terminal surfaces.
1491
+ for model in row.get("models", ()):
1492
+ if isinstance(model, dict):
1493
+ lines.append(
1494
+ f" - {model.get('model', '(unknown)')}: "
1495
+ f"{_terminal_amount(model.get('costUsd'))} · "
1496
+ f"{_terminal_tokens(model.get('totalTokens'))}"
1497
+ )
1498
+ elif command == "range-cost":
1499
+ totals = data.get("totals", {})
1500
+ lines.append(
1501
+ f"Total: {_terminal_amount(totals.get('costUsd'))} · "
1502
+ f"{_terminal_tokens(totals.get('totalTokens'))}"
1503
+ )
1504
+ for row in data.get("models", ()):
1505
+ if isinstance(row, dict):
1506
+ lines.append(f"- {row.get('model', '(unknown)')}: {_terminal_amount(row.get('costUsd'))}")
1507
+ elif command == "cache-report":
1508
+ totals = data.get("totals", {})
1509
+ lines.append(
1510
+ f"Total: {_terminal_amount(totals.get('costUsd'))} · "
1511
+ f"{_terminal_tokens(totals.get('totalTokens'))}"
1512
+ )
1513
+ for section in data.get("sections", ()):
1514
+ if not isinstance(section, dict) or section.get("key") != "token-reuse":
1515
+ continue
1516
+ section_data = section.get("data") or {}
1517
+ for row in section_data.get("rows", ()):
1518
+ if isinstance(row, dict):
1519
+ percent = row.get("cachedInputPercent")
1520
+ rendered = "—" if percent is None else f"{float(percent):.1f}%"
1521
+ lines.append(f"- {row.get('label', 'Codex')}: cached input {rendered}")
1522
+ elif command == "diff":
1523
+ _append_diff_terminal(lines, data)
1524
+ elif command == "report":
1525
+ sections = data.get("sections", ())
1526
+ series = 0
1527
+ for section in sections:
1528
+ if isinstance(section, dict):
1529
+ for source_series in (section.get("data") or {}).get("series", ()):
1530
+ if not isinstance(source_series, dict):
1531
+ continue
1532
+ series += 1
1533
+ slot = source_series.get("slot", "quota")
1534
+ for row in source_series.get("rows", ()):
1535
+ if not isinstance(row, dict):
1536
+ continue
1537
+ lines.append(
1538
+ f"- {slot}: {_terminal_amount(row.get('costUsd'))} · "
1539
+ f"{row.get('usedPercent', '—')}%"
1540
+ )
1541
+ for detail in row.get("detail", ()):
1542
+ if isinstance(detail, dict):
1543
+ lines.append(
1544
+ f" - {detail.get('percentThreshold', '—')}%: "
1545
+ f"{_terminal_amount(detail.get('cumulativeCostUSD'))}"
1546
+ )
1547
+ lines.append(f"Quota series: {series}")
1548
+ return "\n".join(lines)
1549
+
1550
+
1551
+ def _render_claude_terminal(command: str, result: SourceResult) -> str:
1552
+ """Render an all-source Claude section without changing legacy CLI paths."""
1553
+ title = _TERMINAL_TITLES.get(command, "Analytics Report")
1554
+ lines = [f"Claude {title}"]
1555
+ if result.status == "unavailable":
1556
+ return "\n".join([*lines, "Unavailable: Claude analytics are unavailable."])
1557
+ if result.status == "empty":
1558
+ return "\n".join([*lines, "No data."])
1559
+ if command == "diff" and isinstance(result.data, dict):
1560
+ _append_diff_terminal(lines, result.data)
1561
+ return "\n".join(lines)
1562
+ cost, tokens = _legacy_claude_totals(result.data)
1563
+ if cost or tokens:
1564
+ lines.append(f"Total: {_terminal_amount(cost)} · {_terminal_tokens(tokens)}")
1565
+ else:
1566
+ lines.append("Data available.")
1567
+ return "\n".join(lines)
1568
+
1569
+
1570
+ def _emit_source_share(
1571
+ args: object, result: SourceResult, *, command: str,
1572
+ claude: SourceResult | None,
1573
+ ) -> int:
1574
+ """Render one provider snapshot or a Claude-then-Codex composition."""
1575
+ c = _cctally()
1576
+ codex_snap = build_source_share_snapshot(
1577
+ command, result, reveal_projects=bool(getattr(args, "reveal_projects", False)),
1578
+ )
1579
+ if getattr(args, "source", "codex") != "all":
1580
+ c._share_render_and_emit(codex_snap, args)
1581
+ else:
1582
+ if claude is None:
1583
+ raise ValueError("all-source share rendering requires a Claude result")
1584
+ lib = c._share_load_lib()
1585
+ reveal_projects = bool(getattr(args, "reveal_projects", False))
1586
+ claude_snap = build_source_share_snapshot(
1587
+ command, claude, reveal_projects=reveal_projects,
1588
+ )
1589
+ sections = (
1590
+ lib.ComposedSection(
1591
+ snap=lib._scrub(claude_snap, reveal_projects=reveal_projects),
1592
+ drift_detected=False,
1593
+ ),
1594
+ lib.ComposedSection(
1595
+ snap=lib._scrub(codex_snap, reveal_projects=reveal_projects),
1596
+ drift_detected=False,
1597
+ ),
1598
+ )
1599
+ content = lib.compose(
1600
+ sections,
1601
+ opts=lib.ComposeOptions(
1602
+ title=f"{_TERMINAL_TITLES[command]} — Claude + Codex",
1603
+ theme=getattr(args, "theme", "light"),
1604
+ format=args.format,
1605
+ no_branding=bool(getattr(args, "no_branding", False)),
1606
+ reveal_projects=reveal_projects,
1607
+ ),
1608
+ )
1609
+ utc_date = codex_snap.generated_at.astimezone(UTC).strftime("%Y-%m-%d")
1610
+ kind, value = c._resolve_destination(args, cmd=command, generated_at_utc_date=utc_date)
1611
+ c._emit(content, kind=kind, value=value)
1612
+ if getattr(args, "open_after_write", False) and kind == "file":
1613
+ c._share_open_file(value)
1614
+ return 3 if (
1615
+ getattr(args, "project", None)
1616
+ and command in {"range-cost", "cache-report"}
1617
+ and result.status == "unavailable"
1618
+ ) else 0
1619
+
1620
+
1621
+ def _emit_source_result(
1622
+ args: object, result: SourceResult, *, diff: bool = False, report: bool = False,
1623
+ claude: SourceResult | None = None,
1624
+ ) -> int:
1625
+ if getattr(args, "format", None):
1626
+ return _emit_source_share(
1627
+ args, result,
1628
+ command=str(getattr(args, "command", "analytics")),
1629
+ claude=claude,
1630
+ )
1631
+ if getattr(args, "source", "codex") == "all":
1632
+ if claude is None:
1633
+ raise ValueError("all-source rendering requires a Claude result")
1634
+ wire = _all_source_wire(claude, result, diff=diff, report=report)
1635
+ if bool(getattr(args, "json", False)):
1636
+ print(json.dumps(wire, separators=(",", ":")))
1637
+ else:
1638
+ command = str(getattr(args, "command", "analytics"))
1639
+ project_degradation = (
1640
+ getattr(args, "project", None)
1641
+ and command in {"range-cost", "cache-report"}
1642
+ and result.status == "unavailable"
1643
+ )
1644
+ sections = [
1645
+ _render_claude_terminal(command, claude),
1646
+ _render_codex_terminal(
1647
+ command, result, diff=diff,
1648
+ include_unavailable_diagnostic=not project_degradation,
1649
+ ),
1650
+ ]
1651
+ if not report:
1652
+ combined = wire.get("combined")
1653
+ if isinstance(combined, dict):
1654
+ if diff:
1655
+ costs = combined.get("cost_usd", {})
1656
+ tokens = combined.get("total_tokens", {})
1657
+ sections.append(
1658
+ "Combined physical accounting: "
1659
+ f"A {_terminal_amount(costs.get('a') if isinstance(costs, dict) else None)}; "
1660
+ f"B {_terminal_amount(costs.get('b') if isinstance(costs, dict) else None)}; "
1661
+ f"Δ {_terminal_delta_amount(costs.get('delta') if isinstance(costs, dict) else None)}; "
1662
+ f"A {_terminal_tokens(tokens.get('a') if isinstance(tokens, dict) else None)}; "
1663
+ f"B {_terminal_tokens(tokens.get('b') if isinstance(tokens, dict) else None)}; "
1664
+ f"Δ {_terminal_delta_tokens(tokens.get('delta') if isinstance(tokens, dict) else None)}"
1665
+ )
1666
+ else:
1667
+ sections.append(
1668
+ "Combined physical accounting: "
1669
+ f"{_terminal_amount(combined.get('costUsd'))} · "
1670
+ f"{_terminal_tokens(combined.get('totalTokens'))}"
1671
+ )
1672
+ print("\n\n".join(sections))
1673
+ if project_degradation:
1674
+ diagnostic = result.warnings[0].message if result.warnings else "Codex analytics are unavailable."
1675
+ print(f"cctally: {diagnostic}", file=sys.stderr)
1676
+ if (
1677
+ getattr(args, "project", None)
1678
+ and getattr(args, "command", None) in {"range-cost", "cache-report"}
1679
+ and result.status == "unavailable"
1680
+ ):
1681
+ return 3
1682
+ return 0
1683
+ wire = source_result_wire(result, diff=diff)
1684
+ if bool(getattr(args, "json", False)):
1685
+ print(json.dumps(wire, separators=(",", ":")))
1686
+ elif result.status == "unavailable":
1687
+ # Design 10.2 keeps requested-project range/cache direct stdout empty;
1688
+ # the other provider reports carry their stable unavailable diagnostic.
1689
+ if not (
1690
+ getattr(args, "project", None)
1691
+ and getattr(args, "command", None) in {"range-cost", "cache-report"}
1692
+ ):
1693
+ print(_render_codex_terminal(str(getattr(args, "command", "analytics")), result, diff=diff))
1694
+ else:
1695
+ print(_render_codex_terminal(str(getattr(args, "command", "analytics")), result, diff=diff))
1696
+ return 3 if result.status == "unavailable" else 0
1697
+
1698
+
1699
+ def cmd_source_project(args: object) -> int:
1700
+ _cctally()._share_validate_args(args)
1701
+ if getattr(args, "source", "codex") == "claude" and getattr(args, "format", None):
1702
+ return _emit_source_result(args, _run_claude_json_adapter(args, "project"))
1703
+ start, end = _resolve_source_project_range(args)
1704
+ try:
1705
+ validate_source_project_selectors(getattr(args, "project", None))
1706
+ except SourceUsageError as exc:
1707
+ print(f"cctally: {exc}", file=sys.stderr)
1708
+ return 2
1709
+ # Keep the Claude compatibility block on the single calendar interval
1710
+ # selected for the provider-aware command; without this, its legacy
1711
+ # default would silently choose a subscription-week boundary instead.
1712
+ # The provider reader is half-open; the legacy project command renders a
1713
+ # date-only --until as its inclusive final microsecond. These describe
1714
+ # the identical instants, while retaining the frozen Claude JSON labels.
1715
+ claude_end = end
1716
+ raw_until = getattr(args, "until", None)
1717
+ if isinstance(raw_until, str) and not any(
1718
+ marker in raw_until for marker in ("T", "+", "Z")
1719
+ ):
1720
+ claude_end -= dt.timedelta(microseconds=1)
1721
+ args._source_analytics_range = (start, claude_end)
1722
+ claude = (
1723
+ _run_claude_json_adapter(args, "project")
1724
+ if getattr(args, "source", "codex") == "all" else None
1725
+ )
1726
+ try:
1727
+ population_entries = _source_entries(
1728
+ args, start, end, qualified=True, group=getattr(args, "group", "git-root"),
1729
+ )
1730
+ except QualifiedMetadataUnavailable:
1731
+ return _emit_source_result(
1732
+ args, SourceResult("codex", "unavailable", None, (QUALIFIED_METADATA_WARNING,)),
1733
+ claude=claude,
1734
+ )
1735
+ try:
1736
+ entries = _filter_project_entries(population_entries, getattr(args, "project", None))
1737
+ except SourceUsageError as exc:
1738
+ print(f"cctally: {exc}", file=sys.stderr)
1739
+ return 2
1740
+ entries = _filter_model_entries(entries, getattr(args, "model", None))
1741
+ allocation_entries = population_entries
1742
+ quota_available = True
1743
+ supplied_blocks = getattr(args, "blocks", None)
1744
+ if supplied_blocks is None:
1745
+ try:
1746
+ blocks = select_codex_project_blocks(
1747
+ build_blocks(load_codex_quota_observations()),
1748
+ range_start=start, range_end=end, as_of=getattr(args, "as_of", end),
1749
+ )
1750
+ except (OSError, sqlite3.Error, ValueError):
1751
+ # Accounting stays useful when S2's operational state is absent;
1752
+ # omit attributions rather than manufacturing a blended quota.
1753
+ blocks = ()
1754
+ quota_available = False
1755
+ else:
1756
+ blocks = select_codex_project_blocks(
1757
+ tuple(supplied_blocks),
1758
+ range_start=start, range_end=end, as_of=getattr(args, "as_of", end),
1759
+ )
1760
+ if blocks:
1761
+ block_start = min(block.nominal_start_at for block in blocks)
1762
+ block_end = max(
1763
+ min(getattr(args, "as_of", end), block.resets_at) for block in blocks
1764
+ )
1765
+ if block_start < start or block_end > end:
1766
+ try:
1767
+ read_args = copy(args)
1768
+ read_args._source_analytics_sync = False
1769
+ population_entries = _source_entries(
1770
+ read_args,
1771
+ min(start, block_start), max(end, block_end),
1772
+ qualified=True, group=getattr(args, "group", "git-root"),
1773
+ )
1774
+ entries = _filter_project_entries(population_entries, getattr(args, "project", None))
1775
+ entries = _filter_model_entries(entries, getattr(args, "model", None))
1776
+ allocation_entries = population_entries
1777
+ except QualifiedMetadataUnavailable:
1778
+ return _emit_source_result(
1779
+ args, SourceResult("codex", "unavailable", None, (QUALIFIED_METADATA_WARNING,)),
1780
+ claude=claude,
1781
+ )
1782
+ result = build_codex_project_result(
1783
+ entries, range_start=start, range_end=end,
1784
+ blocks=blocks,
1785
+ as_of=getattr(args, "as_of", end),
1786
+ sort=getattr(args, "sort", "cost"), order=getattr(args, "order", "desc"),
1787
+ include_breakdown=bool(getattr(args, "breakdown", False)),
1788
+ allocation_entries=allocation_entries,
1789
+ quota_available=quota_available,
1790
+ )
1791
+ return _emit_source_result(args, result, claude=claude)
1792
+
1793
+
1794
+ def cmd_source_diff(args: object) -> int:
1795
+ _cctally()._share_validate_args(args)
1796
+ if getattr(args, "source", "codex") == "claude" and getattr(args, "format", None):
1797
+ return _emit_source_result(args, _run_claude_json_adapter(args, "diff"), diff=True)
1798
+ window_a, window_b = _resolve_source_diff_windows(args)
1799
+ if getattr(args, "a", None) is None and getattr(args, "b", None) is None:
1800
+ # Direct adapter callers supply resolved AnalyticsWindow instances;
1801
+ # their enclosing test/host owns validation, so preserve the pure
1802
+ # half-open seam without pretending it is a parsed CLI request.
1803
+ normalization = "none"
1804
+ else:
1805
+ try:
1806
+ normalization = _validate_source_diff_controls(args, window_a, window_b)
1807
+ except SourceUsageError as exc:
1808
+ print(f"cctally: {exc}", file=sys.stderr)
1809
+ return 2
1810
+ except ValueError as exc:
1811
+ print(f"diff: {exc}", file=sys.stderr)
1812
+ return 2
1813
+ except RuntimeError as exc:
1814
+ print(str(exc), file=sys.stderr)
1815
+ return 1
1816
+ # The all-source calendar-window contract is resolved once here. The
1817
+ # established Claude renderer consumes the same absolute intervals through
1818
+ # its structured seam instead of attempting subscription-week anchoring.
1819
+ args._source_analytics_windows = (window_a, window_b)
1820
+ source = getattr(args, "source", "codex")
1821
+ claude_only = _diff_only_for_provider(getattr(args, "only", None), "claude")
1822
+ codex_only = _diff_only_for_provider(getattr(args, "only", None), "codex")
1823
+ if source == "all":
1824
+ if claude_only == "":
1825
+ claude = SourceResult("claude", "empty", {"sections": []})
1826
+ else:
1827
+ claude_args = copy(args)
1828
+ claude_args.only = claude_only
1829
+ claude = _run_claude_json_adapter(claude_args, "diff")
1830
+ else:
1831
+ claude = None
1832
+ start, end = min(window_a.start_at, window_b.start_at), max(window_a.end_at, window_b.end_at)
1833
+ requires_project_metadata = (
1834
+ codex_only is None or "projects" in codex_only.split(",")
1835
+ )
1836
+ try:
1837
+ entry_args = copy(args)
1838
+ entry_args._source_analytics_sync = bool(getattr(args, "sync", False))
1839
+ entries = _source_entries(
1840
+ entry_args, start, end, qualified=requires_project_metadata,
1841
+ )
1842
+ metadata_available = True
1843
+ except QualifiedMetadataUnavailable:
1844
+ try:
1845
+ entries = _source_entries(entry_args, start, end, qualified=False)
1846
+ except RuntimeError:
1847
+ return _emit_source_result(
1848
+ args, SourceResult("codex", "unavailable", None, (QUALIFIED_METADATA_WARNING,)),
1849
+ diff=True, claude=claude,
1850
+ )
1851
+ metadata_available = False
1852
+ except RuntimeError:
1853
+ return _emit_source_result(
1854
+ args, SourceResult("codex", "unavailable", None, (QUALIFIED_METADATA_WARNING,)),
1855
+ diff=True, claude=claude,
1856
+ )
1857
+ return _emit_source_result(
1858
+ args,
1859
+ build_codex_diff_result(
1860
+ entries, window_a, window_b,
1861
+ project_metadata_available=metadata_available,
1862
+ only=(codex_only if source == "all" else getattr(args, "only", None)),
1863
+ allow_mismatch=bool(getattr(args, "allow_mismatch", False)),
1864
+ show_all=bool(getattr(args, "show_all", False)),
1865
+ min_delta_usd=(
1866
+ 0.10 if getattr(args, "min_delta_usd", None) is None
1867
+ else float(args.min_delta_usd)
1868
+ ),
1869
+ min_delta_pct=(
1870
+ 1.0 if getattr(args, "min_delta_pct", None) is None
1871
+ else float(args.min_delta_pct)
1872
+ ),
1873
+ sort=str(getattr(args, "sort", "delta")),
1874
+ top=getattr(args, "top", None),
1875
+ normalization=normalization,
1876
+ classify_presence=True,
1877
+ ),
1878
+ diff=True, claude=claude,
1879
+ )
1880
+
1881
+
1882
+ def cmd_source_range_cost(args: object) -> int:
1883
+ _cctally()._share_validate_args(args)
1884
+ if getattr(args, "source", "codex") == "claude" and getattr(args, "format", None):
1885
+ return _emit_source_result(args, _run_claude_json_adapter(args, "range-cost"))
1886
+ start, end = _resolve_source_range_cost_range(args)
1887
+ try:
1888
+ validate_source_project_selectors(getattr(args, "project", None))
1889
+ except SourceUsageError as exc:
1890
+ print(f"cctally: {exc}", file=sys.stderr)
1891
+ return 2
1892
+ claude = (
1893
+ _run_claude_json_adapter(args, "range-cost")
1894
+ if getattr(args, "source", "codex") == "all" else None
1895
+ )
1896
+ try:
1897
+ entries = _source_entries(
1898
+ args, start, end, qualified=bool(getattr(args, "project", None)),
1899
+ inclusive_end=True,
1900
+ )
1901
+ except (QualifiedMetadataUnavailable, RuntimeError):
1902
+ return _emit_source_result(
1903
+ args, SourceResult("codex", "unavailable", None, (QUALIFIED_METADATA_WARNING,)),
1904
+ claude=claude,
1905
+ )
1906
+ try:
1907
+ entries = _filter_project_entries(entries, getattr(args, "project", None))
1908
+ except SourceUsageError as exc:
1909
+ print(f"cctally: {exc}", file=sys.stderr)
1910
+ return 2
1911
+ result = build_codex_range_result(
1912
+ entries, start, end, include_breakdown=bool(getattr(args, "breakdown", False)),
1913
+ )
1914
+ if getattr(args, "total_only", False) and not getattr(args, "format", None):
1915
+ codex_cost, _ = _codex_totals(result)
1916
+ if claude is not None:
1917
+ claude_cost, _ = _legacy_claude_totals(claude.data)
1918
+ codex_cost += claude_cost
1919
+ print(f"{codex_cost:.9f}")
1920
+ return 0
1921
+ return _emit_source_result(args, result, claude=claude)
1922
+
1923
+
1924
+ def cmd_source_cache_report(args: object) -> int:
1925
+ _cctally()._share_validate_args(args)
1926
+ if getattr(args, "source", "codex") == "claude" and getattr(args, "format", None):
1927
+ return _emit_source_result(args, _run_claude_json_adapter(args, "cache-report"))
1928
+ start, end = _resolve_source_cache_range(args)
1929
+ try:
1930
+ validate_source_project_selectors(getattr(args, "project", None))
1931
+ except SourceUsageError as exc:
1932
+ print(f"cctally: {exc}", file=sys.stderr)
1933
+ return 2
1934
+ if getattr(args, "source", "codex") == "all":
1935
+ # ``reuse`` is Codex-only. The all-source Claude leg retains its
1936
+ # ordinary daily/session default rather than receiving an invalid
1937
+ # provider-specific sort; the Codex leg below still uses reuse-desc.
1938
+ claude_args = copy(args)
1939
+ if getattr(claude_args, "sort", None) == "reuse":
1940
+ claude_args.sort = None
1941
+ claude = _run_claude_json_adapter(claude_args, "cache-report")
1942
+ else:
1943
+ claude = None
1944
+ try:
1945
+ entry_args = copy(args)
1946
+ entry_args._source_analytics_sync = True
1947
+ entries = _source_entries(entry_args, start, end, qualified=True)
1948
+ metadata_available = True
1949
+ except QualifiedMetadataUnavailable:
1950
+ if getattr(args, "project", None):
1951
+ return _emit_source_result(
1952
+ args, SourceResult("codex", "unavailable", None, (QUALIFIED_METADATA_WARNING,)),
1953
+ claude=claude,
1954
+ )
1955
+ try:
1956
+ entries = _source_entries(entry_args, start, end, qualified=False)
1957
+ except RuntimeError:
1958
+ return _emit_source_result(
1959
+ args, SourceResult("codex", "unavailable", None, (QUALIFIED_METADATA_WARNING,)),
1960
+ claude=claude,
1961
+ )
1962
+ metadata_available = False
1963
+ try:
1964
+ entries = _filter_project_entries(entries, getattr(args, "project", None))
1965
+ except SourceUsageError as exc:
1966
+ print(f"cctally: {exc}", file=sys.stderr)
1967
+ return 2
1968
+ requested_sort = getattr(args, "sort", None)
1969
+ codex_sort = (
1970
+ requested_sort if requested_sort in {"date", "recent", "cost", "reuse"}
1971
+ else None
1972
+ )
1973
+ result = build_codex_reuse_result(
1974
+ entries,
1975
+ group_by=("session" if getattr(args, "by_session", False)
1976
+ else getattr(args, "group_by", "date")),
1977
+ project_metadata_available=metadata_available,
1978
+ sort=codex_sort,
1979
+ range_start=start,
1980
+ range_end=end,
1981
+ )
1982
+ return _emit_source_result(args, result, claude=claude)
1983
+
1984
+
1985
+ def cmd_source_report(args: object) -> int:
1986
+ _cctally()._share_validate_args(args)
1987
+ if getattr(args, "source", "codex") == "claude" and getattr(args, "format", None):
1988
+ return _emit_source_result(
1989
+ args, _run_claude_json_adapter(args, "report"), report=True,
1990
+ )
1991
+ as_of = _resolve_source_report_as_of(args)
1992
+ claude = (
1993
+ _run_claude_json_adapter(args, "report")
1994
+ if getattr(args, "source", "codex") == "all" else None
1995
+ )
1996
+ supplied_blocks = getattr(args, "blocks", None)
1997
+ already_synced = False
1998
+ force_direct_accounting = False
1999
+ if getattr(args, "sync_current", False) and supplied_blocks is None:
2000
+ c = _cctally()
2001
+ try:
2002
+ cache = c.open_cache_db()
2003
+ try:
2004
+ sync_stats = c.sync_codex_cache(cache)
2005
+ finally:
2006
+ cache.close()
2007
+ c.reconcile_codex_quota_projection(now=as_of)
2008
+ force_direct_accounting = bool(getattr(sync_stats, "lock_contended", False))
2009
+ already_synced = not force_direct_accounting
2010
+ except (OSError, sqlite3.Error, RuntimeError):
2011
+ return _emit_source_result(
2012
+ args, build_codex_report_result((), (), as_of=as_of, quota_available=False),
2013
+ report=True, claude=claude,
2014
+ )
2015
+ if supplied_blocks is None:
2016
+ try:
2017
+ blocks = select_codex_report_blocks(
2018
+ build_blocks(load_codex_quota_observations()),
2019
+ weeks=int(getattr(args, "weeks", 1)),
2020
+ )
2021
+ except (OSError, sqlite3.Error, ValueError):
2022
+ return _emit_source_result(
2023
+ args, build_codex_report_result((), (), as_of=as_of, quota_available=False),
2024
+ report=True, claude=claude,
2025
+ )
2026
+ else:
2027
+ blocks = tuple(supplied_blocks)
2028
+ if blocks:
2029
+ start = min(block.nominal_start_at for block in blocks)
2030
+ end = min(as_of, max(block.resets_at for block in blocks))
2031
+ try:
2032
+ entries = _source_entries(
2033
+ args, start, end, qualified=False, sync=not already_synced,
2034
+ force_direct=force_direct_accounting,
2035
+ )
2036
+ except RuntimeError:
2037
+ return _emit_source_result(
2038
+ args, build_codex_report_result((), (), as_of=as_of, quota_available=False),
2039
+ report=True, claude=claude,
2040
+ )
2041
+ else:
2042
+ entries = tuple(getattr(args, "source_entries", ()))
2043
+ return _emit_source_result(
2044
+ args,
2045
+ build_codex_report_result(
2046
+ entries, blocks, as_of=as_of,
2047
+ include_detail=bool(getattr(args, "detail", False)),
2048
+ ),
2049
+ report=True, claude=claude,
2050
+ )