cctally 1.117.0 → 1.117.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.
- package/CHANGELOG.md +16 -0
- package/bin/_cctally_dashboard.py +39 -10
- package/bin/_cctally_dashboard_envelope.py +10 -0
- package/bin/_cctally_five_hour.py +57 -4
- package/bin/_cctally_forecast.py +272 -0
- package/bin/_cctally_milestone_history.py +379 -91
- package/bin/_cctally_tui.py +362 -33
- package/bin/_lib_dashboard_sources.py +94 -8
- package/bin/_lib_quota_model.py +29 -11
- package/bin/_lib_snapshot_cache.py +8 -0
- package/bin/_lib_tick_stats.py +10 -5
- package/bin/_lib_write_io.py +1 -1
- package/bin/cctally +4 -0
- package/dashboard/static/assets/{ConversationsView-Dcfn8T6f.js → ConversationsView-oqNsWzWV.js} +1 -1
- package/dashboard/static/assets/{DoctorModal-BqmXbnhn.js → DoctorModal-B0DnU9d9.js} +1 -1
- package/dashboard/static/assets/ModalRoot-CYua2wP_.js +1 -0
- package/dashboard/static/assets/{ProjectsDrillPanel-B-HZkDnT.js → ProjectsDrillPanel-DSxXQSa-.js} +1 -1
- package/dashboard/static/assets/{SourceDetailModal-QhJkHB7i.js → SourceDetailModal-BleDpQqG.js} +1 -1
- package/dashboard/static/assets/{UpdateModal-CkXEIQcK.js → UpdateModal-CMqaAkSK.js} +1 -1
- package/dashboard/static/assets/{index-HKS1s56o.js → index-BjQV-0-D.js} +2 -2
- package/dashboard/static/assets/{outlineNavigation-BNGaNPCp.js → outlineNavigation-CO-VWBm7.js} +1 -1
- package/dashboard/static/dashboard.html +1 -1
- package/package.json +1 -1
- package/dashboard/static/assets/ModalRoot-Ck3WJK2V.js +0 -1
package/bin/_cctally_tui.py
CHANGED
|
@@ -183,6 +183,7 @@ from __future__ import annotations
|
|
|
183
183
|
import argparse
|
|
184
184
|
import dataclasses
|
|
185
185
|
import datetime as dt
|
|
186
|
+
import hashlib
|
|
186
187
|
import io
|
|
187
188
|
import json
|
|
188
189
|
import math
|
|
@@ -223,6 +224,7 @@ from _lib_display_tz import (
|
|
|
223
224
|
_compute_display_block,
|
|
224
225
|
)
|
|
225
226
|
from _lib_aggregators import _aggregate_monthly, codex_path_scope
|
|
227
|
+
from _lib_json_envelope import _iso_z
|
|
226
228
|
from _lib_fmt import _fmt_usd_accounting, stable_sum
|
|
227
229
|
# Opt-in backend phase-instrumentation collector (issue #276, Session A). Pure
|
|
228
230
|
# stdlib leaf; near-noop when CCTALLY_PERF_TRACE is unset (phase() returns a
|
|
@@ -299,6 +301,7 @@ from _lib_dashboard_sources import (
|
|
|
299
301
|
calibration_basis_digest,
|
|
300
302
|
calibration_supersession_digest,
|
|
301
303
|
claude_stats_digest,
|
|
304
|
+
claude_weekly_retired_digest,
|
|
302
305
|
codex_stats_digest,
|
|
303
306
|
combined_accounting_version,
|
|
304
307
|
compose_all_state,
|
|
@@ -912,6 +915,18 @@ class TuiCurrentWeek:
|
|
|
912
915
|
# set. Appended last with a default so fixture modules that construct
|
|
913
916
|
# `TuiCurrentWeek` positionally stay valid.
|
|
914
917
|
total_tokens: int = 0
|
|
918
|
+
# #836 (spec 5.1): the displayed account, published as
|
|
919
|
+
# `current_week.account_key` / `account_label` ONLY when Claude is
|
|
920
|
+
# decorated (more than one real account, R8). Filled by the decorated
|
|
921
|
+
# path of `_tui_build_current_week`; the undecorated card leaves all four
|
|
922
|
+
# at their defaults. ``cycle_scope`` is that account's
|
|
923
|
+
# `ClaudeCycleScope`, the one cycle identity the weekly milestones and the
|
|
924
|
+
# scoped history index share with this card. Trailing defaults, for the
|
|
925
|
+
# positional fixture constructors.
|
|
926
|
+
account_key: "str | None" = None
|
|
927
|
+
account_decorated: bool = False
|
|
928
|
+
account_label: "str | None" = None
|
|
929
|
+
cycle_scope: "Any | None" = None
|
|
915
930
|
|
|
916
931
|
|
|
917
932
|
# ---- View-model row dataclasses moved to bin/_lib_view_models.py ----
|
|
@@ -957,10 +972,21 @@ class TuiPercentMilestone:
|
|
|
957
972
|
|
|
958
973
|
def _tui_build_percent_milestones(
|
|
959
974
|
conn: sqlite3.Connection,
|
|
975
|
+
*,
|
|
976
|
+
account_key: "str | None" = None,
|
|
977
|
+
cycle_scope: "Any | None" = None,
|
|
960
978
|
) -> list[TuiPercentMilestone]:
|
|
961
979
|
"""Return per-percent crossings for the current week's ACTIVE
|
|
962
980
|
segment, ascending by percent.
|
|
963
981
|
|
|
982
|
+
#836 (spec 5.3): with ``account_key`` (a decorated store) the week, its
|
|
983
|
+
active segment and the milestone rows all come from ``cycle_scope`` — the
|
|
984
|
+
displayed account's `ClaudeCycleScope`, the identity the card was built
|
|
985
|
+
from — and only that account's rows are read. A decorated build with no
|
|
986
|
+
resolved scope publishes ``[]``, never a merged list. ``account_key=None``
|
|
987
|
+
is today's merged read below, unchanged, including legitimate
|
|
988
|
+
`unattributed` pre-attribution rows in the same week.
|
|
989
|
+
|
|
964
990
|
Resolves `week_start_date` from the latest `weekly_usage_snapshots` row
|
|
965
991
|
— the same path `cmd_percent_breakdown` takes. The post-override
|
|
966
992
|
`TuiCurrentWeek.week_start_at` is NOT suitable here: after a mid-week
|
|
@@ -977,6 +1003,16 @@ def _tui_build_percent_milestones(
|
|
|
977
1003
|
Returns [] if no usage snapshot exists, OR if the active segment
|
|
978
1004
|
has no milestone rows yet (post-credit "fresh" state).
|
|
979
1005
|
"""
|
|
1006
|
+
if account_key is not None:
|
|
1007
|
+
if cycle_scope is None or cycle_scope.account_key != account_key:
|
|
1008
|
+
return []
|
|
1009
|
+
# The scope's segment IS the active one (no second lookup): it was
|
|
1010
|
+
# resolved through the same chokepoint, scoped and as-of bounded.
|
|
1011
|
+
return _tui_percent_milestone_rows([
|
|
1012
|
+
r for r in get_milestones_for_week(
|
|
1013
|
+
conn, cycle_scope.week_start_date, account_key=account_key)
|
|
1014
|
+
if int(r["reset_event_id"] or 0) == int(cycle_scope.reset_event_id)
|
|
1015
|
+
])
|
|
980
1016
|
latest = conn.execute(
|
|
981
1017
|
"SELECT week_start_date, week_end_at FROM weekly_usage_snapshots "
|
|
982
1018
|
"WHERE week_end_at IS NOT NULL "
|
|
@@ -1025,6 +1061,11 @@ def _tui_build_percent_milestones(
|
|
|
1025
1061
|
r for r in get_milestones_for_week(conn, latest["week_start_date"])
|
|
1026
1062
|
if int(r["reset_event_id"] or 0) == active_segment
|
|
1027
1063
|
]
|
|
1064
|
+
return _tui_percent_milestone_rows(rows)
|
|
1065
|
+
|
|
1066
|
+
|
|
1067
|
+
def _tui_percent_milestone_rows(rows) -> list[TuiPercentMilestone]:
|
|
1068
|
+
"""`percent_milestones` rows as `TuiPercentMilestone`s, in row order."""
|
|
1028
1069
|
out: list[TuiPercentMilestone] = []
|
|
1029
1070
|
for r in rows:
|
|
1030
1071
|
try:
|
|
@@ -1394,6 +1435,17 @@ class DataSnapshot:
|
|
|
1394
1435
|
# ingest/read-model work. It remains trailing/defaulted for every legacy
|
|
1395
1436
|
# positional fixture constructor.
|
|
1396
1437
|
source_bundle: SourceDashboardBundle | None = None
|
|
1438
|
+
# #836 spec 5.7 item 2: the earliest instant after this build's as-of at
|
|
1439
|
+
# which evidence the build excluded by that as-of becomes eligible — a
|
|
1440
|
+
# weekly sample captured after it, a reset taking effect (or detected)
|
|
1441
|
+
# after it — or None (`_tui_evidence_eligible_at`). The clock is frozen
|
|
1442
|
+
# before the dispatch signature is read, so such a row is counted by the
|
|
1443
|
+
# signature yet excluded by the build; the idle gate refuses once a tick's
|
|
1444
|
+
# instant reaches this, and the Claude source version carries it, so the
|
|
1445
|
+
# tick that admits the evidence rebuilds and refuses source reuse.
|
|
1446
|
+
# Never published. Idle copies carry it unchanged. LAST, with a default,
|
|
1447
|
+
# for the positional constructors.
|
|
1448
|
+
evidence_eligible_at: dt.datetime | None = None
|
|
1397
1449
|
|
|
1398
1450
|
@classmethod
|
|
1399
1451
|
def synthesize_for_marketing(cls, *, as_of_iso: str) -> "DataSnapshot":
|
|
@@ -1673,6 +1725,138 @@ class RuntimeState:
|
|
|
1673
1725
|
)
|
|
1674
1726
|
|
|
1675
1727
|
|
|
1728
|
+
def _tui_card_freshness(captured, now_utc: dt.datetime):
|
|
1729
|
+
"""``(freshness_label, freshness_age)`` of the card's displayed sample,
|
|
1730
|
+
against the configured oauth_usage thresholds; ``(None, None)`` when the
|
|
1731
|
+
capture is not a datetime. Mirrors the dashboard envelope's cw_freshness
|
|
1732
|
+
derivation in `snapshot_to_envelope`. Refs spec §3.4."""
|
|
1733
|
+
if not isinstance(captured, dt.datetime):
|
|
1734
|
+
return None, None
|
|
1735
|
+
if captured.tzinfo is None:
|
|
1736
|
+
captured = captured.replace(tzinfo=dt.timezone.utc)
|
|
1737
|
+
age_s = max(0.0, (now_utc - captured).total_seconds())
|
|
1738
|
+
try:
|
|
1739
|
+
_fresh_cfg = _get_oauth_usage_config(load_config())
|
|
1740
|
+
except Exception:
|
|
1741
|
+
_fresh_cfg = _OAUTH_USAGE_DEFAULTS
|
|
1742
|
+
return _freshness_label(age_s, _fresh_cfg), int(age_s)
|
|
1743
|
+
|
|
1744
|
+
|
|
1745
|
+
def _tui_five_hour_resets_at(five_hour, now_utc: dt.datetime):
|
|
1746
|
+
"""The five-hour reading's reset instant, or None when absent,
|
|
1747
|
+
unparseable or already elapsed (a stale reset would render as
|
|
1748
|
+
"resets 0h 00m")."""
|
|
1749
|
+
if five_hour is None or five_hour.resets_at is None:
|
|
1750
|
+
return None
|
|
1751
|
+
try:
|
|
1752
|
+
resets_at = parse_iso_datetime(
|
|
1753
|
+
five_hour.resets_at, "five_hour_resets_at")
|
|
1754
|
+
except ValueError:
|
|
1755
|
+
return None
|
|
1756
|
+
return None if resets_at <= now_utc else resets_at
|
|
1757
|
+
|
|
1758
|
+
|
|
1759
|
+
def _claude_current_cycle_scope(*args, **kwargs):
|
|
1760
|
+
return sys.modules["cctally"]._claude_current_cycle_scope(*args, **kwargs)
|
|
1761
|
+
|
|
1762
|
+
|
|
1763
|
+
def _claude_select_current_owner(*args, **kwargs):
|
|
1764
|
+
return sys.modules["cctally"]._claude_select_current_owner(*args, **kwargs)
|
|
1765
|
+
|
|
1766
|
+
|
|
1767
|
+
def _claude_cycle_scope_samples(*args, **kwargs):
|
|
1768
|
+
return sys.modules["cctally"]._claude_cycle_scope_samples(*args, **kwargs)
|
|
1769
|
+
|
|
1770
|
+
|
|
1771
|
+
def _tui_build_decorated_current_week(
|
|
1772
|
+
conn: sqlite3.Connection,
|
|
1773
|
+
now_utc: dt.datetime,
|
|
1774
|
+
*,
|
|
1775
|
+
skip_sync: bool = False,
|
|
1776
|
+
) -> TuiCurrentWeek | None:
|
|
1777
|
+
"""The Current Week card on a decorated store: ONE account (#836,
|
|
1778
|
+
spec 5.2).
|
|
1779
|
+
|
|
1780
|
+
The owner is the account of the newest non-held weekly sample at the
|
|
1781
|
+
as-of instant (`_claude_select_current_owner`), so a newer held row of
|
|
1782
|
+
another account cannot choose the window (D-C). Its cycle identity
|
|
1783
|
+
(`_claude_current_cycle_scope`) supplies the window, the active reset
|
|
1784
|
+
and the effective start, so another account's credit cannot re-anchor it
|
|
1785
|
+
(D-A). Samples, the five-hour reading, the block and the spend are all the
|
|
1786
|
+
owner's (U3, D-B): ``$/1%`` divides the owner's spend by the owner's
|
|
1787
|
+
percentage. When the owner's cycle has no eligible non-held sample (its
|
|
1788
|
+
own reset postdates the chosen one) the card is None; a decorated build
|
|
1789
|
+
never falls back to the merged population. `_apply_midweek_reset_override`
|
|
1790
|
+
is not called here.
|
|
1791
|
+
"""
|
|
1792
|
+
owner = _claude_select_current_owner(conn, now_utc)
|
|
1793
|
+
if owner is None:
|
|
1794
|
+
return None
|
|
1795
|
+
scope = _claude_current_cycle_scope(conn, now_utc, account_key=owner)
|
|
1796
|
+
if scope is None:
|
|
1797
|
+
return None
|
|
1798
|
+
fetched = _claude_cycle_scope_samples(conn, scope, now_utc)
|
|
1799
|
+
if fetched is None:
|
|
1800
|
+
return None
|
|
1801
|
+
week_start_at, week_end_at, observations, week_scope = fetched
|
|
1802
|
+
week_start_before_override = week_start_at
|
|
1803
|
+
if scope.effective_start_at is not None:
|
|
1804
|
+
# The scope's effective start may also PRECEDE the nominal start (a
|
|
1805
|
+
# same-date early re-anchor, #836 review F7): the card's cycle starts
|
|
1806
|
+
# there either way, as its current index entry does.
|
|
1807
|
+
effective = parse_iso_datetime(
|
|
1808
|
+
scope.effective_start_at, "cycle_scope.effective_start_at")
|
|
1809
|
+
if effective != week_start_at:
|
|
1810
|
+
week_start_at = effective
|
|
1811
|
+
observations = [s for s in observations if s[0] >= effective]
|
|
1812
|
+
weekly_samples = [s for s in observations if not s[3]]
|
|
1813
|
+
if not weekly_samples:
|
|
1814
|
+
return None
|
|
1815
|
+
latest = weekly_samples[-1]
|
|
1816
|
+
used_pct = float(latest[1])
|
|
1817
|
+
five_hour = _current_week_five_hour_reading(
|
|
1818
|
+
conn, now_utc, week_scope, account_key=owner,
|
|
1819
|
+
week_start_at=week_start_at,
|
|
1820
|
+
week_start_before_override=week_start_before_override)
|
|
1821
|
+
spent, total_tokens = _sum_cost_and_tokens_for_range(
|
|
1822
|
+
week_start_at, now_utc, mode="auto", skip_sync=skip_sync,
|
|
1823
|
+
account_key=owner,
|
|
1824
|
+
)
|
|
1825
|
+
# The same corrected divisor as the merged card (#661 S2 section 3.4).
|
|
1826
|
+
used_pct_corrected = corrected_percent_point(used_pct)
|
|
1827
|
+
dpp_divisor = used_pct if used_pct_corrected is None else used_pct_corrected
|
|
1828
|
+
dpp = (spent / dpp_divisor) if used_pct > 0 else None
|
|
1829
|
+
freshness_label, freshness_age = _tui_card_freshness(latest[0], now_utc)
|
|
1830
|
+
import _cctally_account
|
|
1831
|
+
return TuiCurrentWeek(
|
|
1832
|
+
week_start_at=week_start_at,
|
|
1833
|
+
week_end_at=week_end_at,
|
|
1834
|
+
used_pct=used_pct,
|
|
1835
|
+
five_hour_pct=None if five_hour is None else five_hour.percent,
|
|
1836
|
+
five_hour_resets_at=_tui_five_hour_resets_at(five_hour, now_utc),
|
|
1837
|
+
spent_usd=float(spent),
|
|
1838
|
+
dollars_per_percent=dpp,
|
|
1839
|
+
latest_snapshot_at=latest[0],
|
|
1840
|
+
freshness_label=freshness_label,
|
|
1841
|
+
freshness_age=freshness_age,
|
|
1842
|
+
five_hour_block=_select_current_block_for_envelope(
|
|
1843
|
+
conn, current_used_pct=used_pct, now_utc=now_utc,
|
|
1844
|
+
account_key=owner,
|
|
1845
|
+
),
|
|
1846
|
+
total_tokens=total_tokens,
|
|
1847
|
+
account_key=owner,
|
|
1848
|
+
account_decorated=True,
|
|
1849
|
+
account_label=_cctally_account.display_account_label(conn, owner),
|
|
1850
|
+
cycle_scope=scope,
|
|
1851
|
+
)
|
|
1852
|
+
|
|
1853
|
+
|
|
1854
|
+
def _decorated_claude(conn: sqlite3.Connection) -> bool:
|
|
1855
|
+
"""The R8 decoration gate for Claude (`provider_is_decorated`)."""
|
|
1856
|
+
import _cctally_account
|
|
1857
|
+
return _cctally_account.provider_is_decorated(conn, "claude")
|
|
1858
|
+
|
|
1859
|
+
|
|
1676
1860
|
def _tui_build_current_week(
|
|
1677
1861
|
conn: sqlite3.Connection,
|
|
1678
1862
|
now_utc: dt.datetime,
|
|
@@ -1682,7 +1866,15 @@ def _tui_build_current_week(
|
|
|
1682
1866
|
"""Build the TuiCurrentWeek from the latest snapshot + live cost.
|
|
1683
1867
|
|
|
1684
1868
|
Returns None when no current-week usage snapshot exists.
|
|
1869
|
+
|
|
1870
|
+
#836: a decorated store (more than one real Claude account, R8) takes
|
|
1871
|
+
`_tui_build_decorated_current_week`, the one-account card. Below the
|
|
1872
|
+
threshold the merged card below is unchanged.
|
|
1685
1873
|
"""
|
|
1874
|
+
import _cctally_account
|
|
1875
|
+
if _cctally_account.provider_is_decorated(conn, "claude"):
|
|
1876
|
+
return _tui_build_decorated_current_week(
|
|
1877
|
+
conn, now_utc, skip_sync=skip_sync)
|
|
1686
1878
|
# #769 S11 (#824): ONE read, held-inclusive, split per axis below.
|
|
1687
1879
|
# Retain the owner of the exact non-held weekly sample this card displays,
|
|
1688
1880
|
# including when one real account coexists with unattributed history. R8
|
|
@@ -1758,35 +1950,11 @@ def _tui_build_current_week(
|
|
|
1758
1950
|
# percent and the reset together from one row), so the countdown never
|
|
1759
1951
|
# describes another row. That reading is held-inclusive (#769 S11, #824),
|
|
1760
1952
|
# because a held row's five-hour fields are the writing tick's own.
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
)
|
|
1767
|
-
except ValueError:
|
|
1768
|
-
five_hr_resets_at = None
|
|
1769
|
-
# Suppress stale resets that have already elapsed so renderers
|
|
1770
|
-
# don't show "resets 0h 00m" or a negative duration at the boundary.
|
|
1771
|
-
if five_hr_resets_at is not None and five_hr_resets_at <= now_utc:
|
|
1772
|
-
five_hr_resets_at = None
|
|
1773
|
-
# Freshness — compute label/age from latest snapshot vs. now using the
|
|
1774
|
-
# configured oauth_usage thresholds. Mirrors the dashboard envelope's
|
|
1775
|
-
# cw_freshness derivation in `snapshot_to_envelope`. Refs spec §3.4.
|
|
1776
|
-
captured = latest[0]
|
|
1777
|
-
if isinstance(captured, dt.datetime):
|
|
1778
|
-
if captured.tzinfo is None:
|
|
1779
|
-
captured = captured.replace(tzinfo=dt.timezone.utc)
|
|
1780
|
-
age_s = max(0.0, (now_utc - captured).total_seconds())
|
|
1781
|
-
try:
|
|
1782
|
-
_fresh_cfg = _get_oauth_usage_config(load_config())
|
|
1783
|
-
except Exception:
|
|
1784
|
-
_fresh_cfg = _OAUTH_USAGE_DEFAULTS
|
|
1785
|
-
freshness_label = _freshness_label(age_s, _fresh_cfg)
|
|
1786
|
-
freshness_age = int(age_s)
|
|
1787
|
-
else:
|
|
1788
|
-
freshness_label = None
|
|
1789
|
-
freshness_age = None
|
|
1953
|
+
# Suppress stale resets that have already elapsed so renderers don't
|
|
1954
|
+
# show "resets 0h 00m" or a negative duration at the boundary.
|
|
1955
|
+
five_hr_resets_at = _tui_five_hour_resets_at(five_hour, now_utc)
|
|
1956
|
+
# Freshness — label/age from the displayed sample vs. now.
|
|
1957
|
+
freshness_label, freshness_age = _tui_card_freshness(latest[0], now_utc)
|
|
1790
1958
|
return TuiCurrentWeek(
|
|
1791
1959
|
week_start_at=week_start_at,
|
|
1792
1960
|
week_end_at=week_end_at,
|
|
@@ -2427,7 +2595,72 @@ def _snapshot_data_version(sig) -> str:
|
|
|
2427
2595
|
# moves no numeric leg above, so without this the detail endpoints' change
|
|
2428
2596
|
# signal stays flat across a tick that added an alert row.
|
|
2429
2597
|
claude_digest = getattr(sig, "claude_stats_digest", "")
|
|
2430
|
-
|
|
2598
|
+
out = out if not claude_digest else f"{out}.x{claude_digest}"
|
|
2599
|
+
# #836 spec 5.7 item 1: the Claude weekly rejection state. Empty on a store
|
|
2600
|
+
# with no retired snapshot and no unavailable five-hour join, so it is
|
|
2601
|
+
# byte-neutral there.
|
|
2602
|
+
retired = getattr(sig, "claude_weekly_retired_digest", "")
|
|
2603
|
+
return out if not retired else f"{out}.r{retired}"
|
|
2604
|
+
|
|
2605
|
+
|
|
2606
|
+
def _tui_evidence_eligible_at(conn, as_of_utc: dt.datetime) -> "dt.datetime | None":
|
|
2607
|
+
"""The earliest instant after ``as_of_utc`` at which evidence a build at
|
|
2608
|
+
that as-of excluded becomes eligible, or None (#836, spec 5.7 item 2).
|
|
2609
|
+
|
|
2610
|
+
The candidates are the rows a later instant's build could admit:
|
|
2611
|
+
|
|
2612
|
+
* weekly snapshots captured after ``as_of_utc`` whose `week_start_date` is
|
|
2613
|
+
at or after the as-of date minus eight days — every boundaryful window
|
|
2614
|
+
that can contain the as-of instant (including another account's older
|
|
2615
|
+
overlapping window), the date-only leg, a later window and the
|
|
2616
|
+
no-current-card case. That leading range is an `idx_usage_week_time`
|
|
2617
|
+
range, never a whole-table scan; held rows are included, since the
|
|
2618
|
+
five-hour reading reads them;
|
|
2619
|
+
* `week_reset_events` rows taking effect after it (the decorated cycle
|
|
2620
|
+
admits a reset by its effective instant) or detected after it (the
|
|
2621
|
+
merged override admits one by its detection instant).
|
|
2622
|
+
|
|
2623
|
+
Instants are compared with `unixepoch`, never lexically: the columns carry
|
|
2624
|
+
mixed offset spellings. A far-future row (a clock error) only postpones
|
|
2625
|
+
the one rebuild; it never disables the idle path.
|
|
2626
|
+
"""
|
|
2627
|
+
as_of = as_of_utc.astimezone(dt.timezone.utc)
|
|
2628
|
+
as_of_iso = _iso_z(as_of)
|
|
2629
|
+
floor_date = (as_of.date() - dt.timedelta(days=8)).isoformat()
|
|
2630
|
+
candidates: list[str] = [
|
|
2631
|
+
row[0] for row in conn.execute(
|
|
2632
|
+
"SELECT captured_at_utc FROM weekly_usage_snapshots "
|
|
2633
|
+
"WHERE week_start_date >= ? "
|
|
2634
|
+
"AND unixepoch(captured_at_utc) > unixepoch(?)",
|
|
2635
|
+
(floor_date, as_of_iso)).fetchall()
|
|
2636
|
+
]
|
|
2637
|
+
try:
|
|
2638
|
+
for effective, detected in conn.execute(
|
|
2639
|
+
"SELECT effective_reset_at_utc, detected_at_utc "
|
|
2640
|
+
"FROM week_reset_events "
|
|
2641
|
+
"WHERE unixepoch(effective_reset_at_utc) > unixepoch(?) "
|
|
2642
|
+
" OR unixepoch(detected_at_utc) > unixepoch(?)",
|
|
2643
|
+
(as_of_iso, as_of_iso)).fetchall():
|
|
2644
|
+
candidates.extend(v for v in (effective, detected) if v)
|
|
2645
|
+
except sqlite3.OperationalError as exc:
|
|
2646
|
+
if "no such table" not in str(exc).lower():
|
|
2647
|
+
raise
|
|
2648
|
+
eligible: "dt.datetime | None" = None
|
|
2649
|
+
for text in candidates:
|
|
2650
|
+
try:
|
|
2651
|
+
instant = parse_iso_datetime(text, "evidence_eligible_at")
|
|
2652
|
+
except (TypeError, ValueError):
|
|
2653
|
+
continue
|
|
2654
|
+
if instant > as_of and (eligible is None or instant < eligible):
|
|
2655
|
+
eligible = instant
|
|
2656
|
+
return eligible
|
|
2657
|
+
|
|
2658
|
+
|
|
2659
|
+
def _tui_evidence_matured(prior, now_utc: dt.datetime) -> bool:
|
|
2660
|
+
"""Has evidence ``prior`` excluded by its as-of become eligible at
|
|
2661
|
+
``now_utc``? (#836, spec 5.7 item 2: the idle gate refuses then.)"""
|
|
2662
|
+
eligible = getattr(prior, "evidence_eligible_at", None)
|
|
2663
|
+
return eligible is not None and now_utc >= eligible
|
|
2431
2664
|
|
|
2432
2665
|
|
|
2433
2666
|
def _tui_source_copy(value: object) -> object:
|
|
@@ -2490,6 +2723,35 @@ def alert_row_owner(
|
|
|
2490
2723
|
raise ValueError(f"no ownership rule for alert axis {axis!r}")
|
|
2491
2724
|
|
|
2492
2725
|
|
|
2726
|
+
def _tui_claude_owner_version_fragment(claude_data: object) -> str:
|
|
2727
|
+
"""The decorated Claude source-version fragment (#836 spec 5.7), or "".
|
|
2728
|
+
|
|
2729
|
+
A digest of the displayed account, its label and the published scoped
|
|
2730
|
+
history index's identity (each entry's key, detail stamp and current
|
|
2731
|
+
flag), read from the already-projected source data. Only a decorated
|
|
2732
|
+
build publishes ``current_week.account_key``, so every undecorated build
|
|
2733
|
+
returns "" and its version bytes stay as they were.
|
|
2734
|
+
"""
|
|
2735
|
+
hero = claude_data.get("hero") if isinstance(claude_data, dict) else None
|
|
2736
|
+
cw = hero.get("current_week") if isinstance(hero, dict) else None
|
|
2737
|
+
if not isinstance(cw, dict):
|
|
2738
|
+
return ""
|
|
2739
|
+
owner = cw.get("account_key")
|
|
2740
|
+
if not isinstance(owner, str) or not owner:
|
|
2741
|
+
return ""
|
|
2742
|
+
index = cw.get("week_index")
|
|
2743
|
+
entries = [
|
|
2744
|
+
(e.get("key"), e.get("detail_stamp"), bool(e.get("is_current")))
|
|
2745
|
+
for e in (index if isinstance(index, list) else ())
|
|
2746
|
+
if isinstance(e, dict)
|
|
2747
|
+
]
|
|
2748
|
+
material = json.dumps(
|
|
2749
|
+
[owner, cw.get("account_label"), entries],
|
|
2750
|
+
sort_keys=True, separators=(",", ":"), ensure_ascii=True,
|
|
2751
|
+
)
|
|
2752
|
+
return hashlib.sha256(material.encode("utf-8")).hexdigest()[:16]
|
|
2753
|
+
|
|
2754
|
+
|
|
2493
2755
|
def _tui_project_claude_source_data(legacy_envelope: object) -> dict[str, object]:
|
|
2494
2756
|
"""Project one completed Claude legacy envelope without further DB reads.
|
|
2495
2757
|
|
|
@@ -3843,9 +4105,14 @@ def _tui_build_source_bundle(
|
|
|
3843
4105
|
prior_bundle: SourceDashboardBundle | None = None,
|
|
3844
4106
|
raw_config: dict[str, object] | None = None,
|
|
3845
4107
|
attempt: dict[str, bool] | None = None,
|
|
4108
|
+
evidence_eligible_at: dt.datetime | None = None,
|
|
3846
4109
|
) -> SourceDashboardBundle:
|
|
3847
4110
|
"""Build one frozen source bundle after the dashboard's coordinated ingest.
|
|
3848
4111
|
|
|
4112
|
+
``evidence_eligible_at`` (#836, spec 5.7 item 2) is the build's
|
|
4113
|
+
`DataSnapshot.evidence_eligible_at`; it enters the Claude version only
|
|
4114
|
+
when set.
|
|
4115
|
+
|
|
3849
4116
|
This helper is reachable only from ``precompute_envelope=True``. It opens
|
|
3850
4117
|
a fresh cache handle after the ingest handle has closed, then performs
|
|
3851
4118
|
read-only provider adaptation with no implicit sync or rollout fallback.
|
|
@@ -3983,6 +4250,9 @@ def _tui_build_source_bundle(
|
|
|
3983
4250
|
# rare event and a rebuild is safe, so per-provider narrowing is not worth
|
|
3984
4251
|
# the split.
|
|
3985
4252
|
accounts_digest = accounts_identity_digest(stats_conn)
|
|
4253
|
+
# #836 spec 5.7 item 1: the Claude weekly rejection state; "" (and so
|
|
4254
|
+
# byte-neutral) on a store with nothing retired or unavailable.
|
|
4255
|
+
retired_digest = claude_weekly_retired_digest(stats_conn)
|
|
3986
4256
|
with _perf.phase("source.signature"):
|
|
3987
4257
|
signature = c.compute_signature(
|
|
3988
4258
|
cache_conn,
|
|
@@ -3991,6 +4261,7 @@ def _tui_build_source_bundle(
|
|
|
3991
4261
|
codex_stats_digest=stats_digest,
|
|
3992
4262
|
accounts_digest=accounts_digest,
|
|
3993
4263
|
claude_stats_digest=claude_digest,
|
|
4264
|
+
claude_weekly_retired_digest=retired_digest,
|
|
3994
4265
|
)
|
|
3995
4266
|
# #857: the Codex quota-dependency identity, read on the SAME pinned
|
|
3996
4267
|
# cache snapshot the Codex capture below reads, so a generation built
|
|
@@ -4058,6 +4329,23 @@ def _tui_build_source_bundle(
|
|
|
4058
4329
|
f":p{_period_identity}{_budget_suffix}{_acct_suffix}"
|
|
4059
4330
|
f"{_aggregate_suffix}:x{claude_digest}"
|
|
4060
4331
|
)
|
|
4332
|
+
# #836 spec 5.7: exact-version reuse compares this string, so a
|
|
4333
|
+
# retirement or restoration (`:r`) and a tick that admits evidence an
|
|
4334
|
+
# earlier one excluded (`:e`) both refuse the prior Claude object. Each
|
|
4335
|
+
# is appended only when set, so other stores keep their version bytes.
|
|
4336
|
+
if retired_digest:
|
|
4337
|
+
claude_version = f"{claude_version}:r{retired_digest}"
|
|
4338
|
+
if evidence_eligible_at is not None:
|
|
4339
|
+
claude_version = f"{claude_version}:e{_iso_z(evidence_eligible_at)}"
|
|
4340
|
+
# #836 spec 5.7: the decorated-only fragment. A decorated Claude source
|
|
4341
|
+
# publishes ONE account's card and that account's history index, and
|
|
4342
|
+
# exact-version reuse compares this string, so the owner, its label
|
|
4343
|
+
# and the published index identity (keys, stamps, current flag) enter
|
|
4344
|
+
# it. Undecorated envelopes carry no `account_key`, so their version
|
|
4345
|
+
# bytes are unchanged.
|
|
4346
|
+
_owner_fragment = _tui_claude_owner_version_fragment(claude_data)
|
|
4347
|
+
if _owner_fragment:
|
|
4348
|
+
claude_version = f"{claude_version}:o{_owner_fragment}"
|
|
4061
4349
|
prior_claude = (
|
|
4062
4350
|
prior_bundle.sources.get("claude")
|
|
4063
4351
|
if prior_bundle is not None else None
|
|
@@ -5240,7 +5528,10 @@ def _tui_build_snapshot_once(
|
|
|
5240
5528
|
)
|
|
5241
5529
|
)
|
|
5242
5530
|
and not _snapshot_period_rolled_over(
|
|
5243
|
-
prior_snap, now_utc, _build_display_tz)
|
|
5531
|
+
prior_snap, now_utc, _build_display_tz)
|
|
5532
|
+
# #836 spec 5.7 item 2: evidence the prior build
|
|
5533
|
+
# excluded by its as-of is eligible now.
|
|
5534
|
+
and not _tui_evidence_matured(prior_snap, now_utc)):
|
|
5244
5535
|
with _perf.phase("idle-decision"):
|
|
5245
5536
|
idle_snap = _tui_build_idle_snapshot(
|
|
5246
5537
|
prior_snap, now_utc=now_utc,
|
|
@@ -5405,7 +5696,14 @@ def _tui_build_snapshot_once(
|
|
|
5405
5696
|
with _perf.phase("build.milestones"):
|
|
5406
5697
|
try:
|
|
5407
5698
|
if cw is not None:
|
|
5408
|
-
|
|
5699
|
+
# #836 (spec 5.3): a decorated card's milestones are its
|
|
5700
|
+
# owner's, from the card's own cycle identity.
|
|
5701
|
+
if getattr(cw, "account_decorated", False):
|
|
5702
|
+
milestones = _tui_build_percent_milestones(
|
|
5703
|
+
conn, account_key=cw.account_key,
|
|
5704
|
+
cycle_scope=cw.cycle_scope)
|
|
5705
|
+
else:
|
|
5706
|
+
milestones = _tui_build_percent_milestones(conn)
|
|
5409
5707
|
except Exception as exc:
|
|
5410
5708
|
capture_failure("milestones", "stats", exc)
|
|
5411
5709
|
history: list = []
|
|
@@ -5597,7 +5895,22 @@ def _tui_build_snapshot_once(
|
|
|
5597
5895
|
week_index: list[dict] = []
|
|
5598
5896
|
with _perf.phase("build.week_index"):
|
|
5599
5897
|
try:
|
|
5600
|
-
|
|
5898
|
+
# #836 spec 5.5 (U1): a decorated card names its account and
|
|
5899
|
+
# its cycle, and the index is that account's history derived
|
|
5900
|
+
# with the SAME scope, so the current entry is the card's
|
|
5901
|
+
# cycle. Undecorated: today's merged index. A decorated store
|
|
5902
|
+
# with no card publishes no index (it rides `current_week`),
|
|
5903
|
+
# so the merged one is not built for it (review F9); the
|
|
5904
|
+
# extra predicate runs only when there is no card.
|
|
5905
|
+
if cw is not None and getattr(cw, "account_decorated", False):
|
|
5906
|
+
week_index = sys.modules["cctally"].build_claude_week_index(
|
|
5907
|
+
conn, account_key=cw.account_key,
|
|
5908
|
+
cycle_scope=cw.cycle_scope)
|
|
5909
|
+
elif cw is None and _decorated_claude(conn):
|
|
5910
|
+
week_index = []
|
|
5911
|
+
else:
|
|
5912
|
+
week_index = sys.modules["cctally"].build_claude_week_index(
|
|
5913
|
+
conn)
|
|
5601
5914
|
except Exception as exc:
|
|
5602
5915
|
capture_failure("week-index", "stats", exc)
|
|
5603
5916
|
# ---- Projects panel + modal envelope (spec §5.2, plan Task 1) -----
|
|
@@ -5784,6 +6097,16 @@ def _tui_build_snapshot_once(
|
|
|
5784
6097
|
daily_panel, now_utc=now_utc, display_tz=_build_display_tz,
|
|
5785
6098
|
)
|
|
5786
6099
|
|
|
6100
|
+
# #836 spec 5.7 item 2: recorded on every dashboard (non-idle) build;
|
|
6101
|
+
# the idle gate and the Claude source version read it.
|
|
6102
|
+
evidence_eligible_at: dt.datetime | None = None
|
|
6103
|
+
if precompute_envelope:
|
|
6104
|
+
with _perf.phase("build.evidence_eligibility"):
|
|
6105
|
+
try:
|
|
6106
|
+
evidence_eligible_at = _tui_evidence_eligible_at(
|
|
6107
|
+
conn, now_utc)
|
|
6108
|
+
except Exception as exc:
|
|
6109
|
+
capture_failure("evidence-eligibility", "stats", exc)
|
|
5787
6110
|
snap = DataSnapshot(
|
|
5788
6111
|
current_week=cw,
|
|
5789
6112
|
forecast=fc,
|
|
@@ -5821,6 +6144,7 @@ def _tui_build_snapshot_once(
|
|
|
5821
6144
|
envelope_precompute=envelope_precompute_block,
|
|
5822
6145
|
data_version=data_version,
|
|
5823
6146
|
source_bundle=None,
|
|
6147
|
+
evidence_eligible_at=evidence_eligible_at,
|
|
5824
6148
|
)
|
|
5825
6149
|
if precompute_envelope:
|
|
5826
6150
|
source_bundle: SourceDashboardBundle | None = None
|
|
@@ -5872,6 +6196,7 @@ def _tui_build_snapshot_once(
|
|
|
5872
6196
|
projects_envelope=projects_envelope_block,
|
|
5873
6197
|
prior_bundle=prior_source_bundle,
|
|
5874
6198
|
raw_config=raw_config,
|
|
6199
|
+
evidence_eligible_at=evidence_eligible_at,
|
|
5875
6200
|
)
|
|
5876
6201
|
if source_bundle is None:
|
|
5877
6202
|
raise RuntimeError("source bundle builder returned no bundle")
|
|
@@ -6127,6 +6452,8 @@ def _tui_compute_dispatch_signature(stats_conn, *, with_codex_dependency=False):
|
|
|
6127
6452
|
codex_stats_digest=codex_stats_digest(stats_conn),
|
|
6128
6453
|
accounts_digest=accounts_identity_digest(stats_conn),
|
|
6129
6454
|
claude_stats_digest=claude_stats_digest(stats_conn),
|
|
6455
|
+
claude_weekly_retired_digest=(
|
|
6456
|
+
claude_weekly_retired_digest(stats_conn)),
|
|
6130
6457
|
)
|
|
6131
6458
|
if not with_codex_dependency:
|
|
6132
6459
|
return signature
|
|
@@ -6368,6 +6695,8 @@ def _tui_build_idle_snapshot(prior, *, now_utc, precompute_envelope,
|
|
|
6368
6695
|
prior_bundle=source_bundle,
|
|
6369
6696
|
raw_config=raw_config,
|
|
6370
6697
|
attempt=source_attempt,
|
|
6698
|
+
evidence_eligible_at=getattr(
|
|
6699
|
+
prior, "evidence_eligible_at", None),
|
|
6371
6700
|
)
|
|
6372
6701
|
except _StatsSnapshotCorruption:
|
|
6373
6702
|
raise
|