cctally 1.82.0 → 1.83.0

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.
Files changed (52) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/README.md +52 -74
  3. package/bin/_cctally_alerts.py +8 -1
  4. package/bin/_cctally_cache.py +963 -149
  5. package/bin/_cctally_config.py +43 -4
  6. package/bin/_cctally_core.py +933 -759
  7. package/bin/_cctally_dashboard.py +157 -47
  8. package/bin/_cctally_dashboard_cache_report.py +13 -6
  9. package/bin/_cctally_dashboard_conversation.py +1 -0
  10. package/bin/_cctally_dashboard_envelope.py +186 -8
  11. package/bin/_cctally_dashboard_share.py +60 -20
  12. package/bin/_cctally_dashboard_sources.py +427 -128
  13. package/bin/_cctally_db.py +605 -128
  14. package/bin/_cctally_doctor.py +413 -28
  15. package/bin/_cctally_five_hour.py +12 -5
  16. package/bin/_cctally_journal.py +2050 -156
  17. package/bin/_cctally_journal_repair.py +519 -0
  18. package/bin/_cctally_milestone_history.py +142 -56
  19. package/bin/_cctally_milestones.py +179 -111
  20. package/bin/_cctally_parser.py +42 -0
  21. package/bin/_cctally_project.py +24 -18
  22. package/bin/_cctally_quota.py +139 -25
  23. package/bin/_cctally_record.py +279 -108
  24. package/bin/_cctally_rederive.py +1052 -0
  25. package/bin/_cctally_reporting.py +58 -53
  26. package/bin/_cctally_setup.py +1 -0
  27. package/bin/_cctally_source_analytics.py +4 -1
  28. package/bin/_cctally_statusline.py +11 -11
  29. package/bin/_cctally_store.py +1039 -31
  30. package/bin/_cctally_sync_week.py +17 -8
  31. package/bin/_cctally_tui.py +421 -54
  32. package/bin/_cctally_update.py +133 -8
  33. package/bin/_cctally_weekrefs.py +14 -0
  34. package/bin/_lib_aggregators.py +10 -6
  35. package/bin/_lib_cache_report.py +101 -9
  36. package/bin/_lib_codex_pools.py +82 -0
  37. package/bin/_lib_conversation_query.py +126 -33
  38. package/bin/_lib_dashboard_sources.py +126 -1
  39. package/bin/_lib_diff_kernel.py +28 -15
  40. package/bin/_lib_doctor.py +342 -4
  41. package/bin/_lib_journal.py +924 -2
  42. package/bin/_lib_jsonl.py +43 -14
  43. package/bin/_lib_pricing.py +140 -21
  44. package/bin/_lib_readme_refresh.py +401 -0
  45. package/bin/_lib_rederive.py +395 -0
  46. package/bin/_lib_share.py +58 -2
  47. package/bin/cctally +56 -8
  48. package/dashboard/static/assets/{index-DJP4gEB7.js → index-3bgCMVHb.js} +52 -52
  49. package/dashboard/static/assets/index-D27EIHEI.css +1 -0
  50. package/dashboard/static/dashboard.html +2 -2
  51. package/package.json +6 -1
  52. package/dashboard/static/assets/index-Dk1nplOz.css +0 -1
@@ -47,8 +47,9 @@ from zoneinfo import ZoneInfo
47
47
  from _cctally_core import open_db, parse_iso_datetime
48
48
  from _cctally_config import save_config, _load_config_unlocked
49
49
  from _lib_fmt import stable_sum
50
- from _lib_pricing import _calculate_entry_cost
50
+ from _lib_pricing import _calculate_entry_cost, claude_usage_dict
51
51
  from _lib_five_hour import _canonical_5h_window_key
52
+ from _lib_dashboard_sources import source_domain_freshness
52
53
 
53
54
 
54
55
  # Share-CLI helpers consumed by the dashboard's share-data builders.
@@ -395,12 +396,14 @@ def _share_top_projects_for_range(
395
396
  # an empty top_projects.
396
397
  return []
397
398
  for entry in entries:
398
- usage = {
399
- "input_tokens": entry.input_tokens,
400
- "output_tokens": entry.output_tokens,
401
- "cache_creation_input_tokens": entry.cache_creation_tokens,
402
- "cache_read_input_tokens": entry.cache_read_tokens,
403
- }
399
+ usage = claude_usage_dict( # #195 chokepoint
400
+ input_tokens=entry.input_tokens,
401
+ output_tokens=entry.output_tokens,
402
+ cache_creation_tokens=entry.cache_creation_tokens,
403
+ cache_read_tokens=entry.cache_read_tokens,
404
+ cache_1h_tokens=getattr(entry, "cache_1h_tokens", None),
405
+ speed=getattr(entry, "speed", None),
406
+ )
404
407
  cost = _calculate_entry_cost(
405
408
  entry.model, usage, mode="auto", cost_usd=entry.cost_usd,
406
409
  )
@@ -434,12 +437,14 @@ def _share_all_projects_for_range(
434
437
  except Exception:
435
438
  return bucket
436
439
  for entry in entries:
437
- usage = {
438
- "input_tokens": entry.input_tokens,
439
- "output_tokens": entry.output_tokens,
440
- "cache_creation_input_tokens": entry.cache_creation_tokens,
441
- "cache_read_input_tokens": entry.cache_read_tokens,
442
- }
440
+ usage = claude_usage_dict( # #195 chokepoint
441
+ input_tokens=entry.input_tokens,
442
+ output_tokens=entry.output_tokens,
443
+ cache_creation_tokens=entry.cache_creation_tokens,
444
+ cache_read_tokens=entry.cache_read_tokens,
445
+ cache_1h_tokens=getattr(entry, "cache_1h_tokens", None),
446
+ speed=getattr(entry, "speed", None),
447
+ )
443
448
  cost = _calculate_entry_cost(
444
449
  entry.model, usage, mode="auto", cost_usd=entry.cost_usd,
445
450
  )
@@ -477,12 +482,14 @@ def _share_per_day_per_project_for_range(
477
482
  except Exception:
478
483
  return out
479
484
  for entry in entries:
480
- usage = {
481
- "input_tokens": entry.input_tokens,
482
- "output_tokens": entry.output_tokens,
483
- "cache_creation_input_tokens": entry.cache_creation_tokens,
484
- "cache_read_input_tokens": entry.cache_read_tokens,
485
- }
485
+ usage = claude_usage_dict( # #195 chokepoint
486
+ input_tokens=entry.input_tokens,
487
+ output_tokens=entry.output_tokens,
488
+ cache_creation_tokens=entry.cache_creation_tokens,
489
+ cache_read_tokens=entry.cache_read_tokens,
490
+ cache_1h_tokens=getattr(entry, "cache_1h_tokens", None),
491
+ speed=getattr(entry, "speed", None),
492
+ )
486
493
  cost = _calculate_entry_cost(
487
494
  entry.model, usage, mode="auto", cost_usd=entry.cost_usd,
488
495
  )
@@ -1504,7 +1511,16 @@ def _build_codex_source_share_snapshot(ls, *, state, panel: str,
1504
1511
  continue
1505
1512
  forecast = row.get("forecast") if isinstance(row.get("forecast"), Mapping) else {}
1506
1513
  current = row.get("current_percent")
1507
- projected = forecast.get("projected_percent")
1514
+ # #350 spec §3.6 — "Projections blank. Actuals stay." BOTH the build
1515
+ # and the idle clock deliberately preserve `projected_percent`
1516
+ # alongside a non-ok `status`, and this site formatted it with no
1517
+ # status check at all. That was masked while a stale Codex source
1518
+ # collapsed to `unavailable` here; §3.4 keeps the source coherent, so
1519
+ # a shared forecast would otherwise publish a stale projection.
1520
+ projected = (
1521
+ forecast.get("projected_percent")
1522
+ if forecast.get("status") == "ok" else None
1523
+ )
1508
1524
  rows.append(ls.Row(cells={
1509
1525
  "limit": ls.TextCell(str(row.get("label") or "Codex quota")),
1510
1526
  "current": ls.TextCell("—" if current is None else f"{float(current):.1f}%"),
@@ -1636,6 +1652,20 @@ def _share_state_domain(state, panel: str):
1636
1652
  return data.get(panel)
1637
1653
 
1638
1654
 
1655
+ def _share_apply_current_week_freshness(snapshot, state, panel: str):
1656
+ """Qualify retained current-week actuals with provider-local evidence age."""
1657
+ if (
1658
+ panel != "current-week"
1659
+ or source_domain_freshness(state, "hero") != "stale"
1660
+ ):
1661
+ return snapshot
1662
+ provider = "Claude" if state.source == "claude" else "Codex"
1663
+ note = (
1664
+ f"{provider} current-week spend is based on stale provider-cycle evidence."
1665
+ )
1666
+ return replace(snapshot, notes=tuple(snapshot.notes) + (note,))
1667
+
1668
+
1639
1669
  def _share_digest_input(*, panel: str, template_id: str, source: str,
1640
1670
  source_explicit: bool, states, snapshots,
1641
1671
  panel_data, account: "str | None" = None):
@@ -1656,6 +1686,10 @@ def _share_digest_input(*, panel: str, template_id: str, source: str,
1656
1686
  "source": state.source,
1657
1687
  "data_version": state.data_version,
1658
1688
  "availability": state.availability,
1689
+ **(
1690
+ {"hero_freshness": source_domain_freshness(state, "hero")}
1691
+ if panel == "current-week" else {}
1692
+ ),
1659
1693
  "period": {
1660
1694
  "start": snapshot.period.start,
1661
1695
  "end": snapshot.period.end,
@@ -1703,6 +1737,9 @@ def _share_build_source_snapshots(*, ls, template, template_id: str,
1703
1737
  # does not carry the additive S4 source bundle.
1704
1738
  if source_explicit or source == "all":
1705
1739
  claude_state = _source_state_for_share(data_snap, "claude")
1740
+ claude_snapshot = _share_apply_current_week_freshness(
1741
+ claude_snapshot, claude_state, panel,
1742
+ )
1706
1743
 
1707
1744
  codex_snapshot = None
1708
1745
  codex_state = None
@@ -1717,6 +1754,9 @@ def _share_build_source_snapshots(*, ls, template, template_id: str,
1717
1754
  template_id=template_id,
1718
1755
  options=options,
1719
1756
  )
1757
+ codex_snapshot = _share_apply_current_week_freshness(
1758
+ codex_snapshot, codex_state, panel,
1759
+ )
1720
1760
 
1721
1761
  if source == "claude":
1722
1762
  return (claude_snapshot,), (claude_state,), panel_data