cctally 1.100.0 → 1.102.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.
- package/CHANGELOG.md +73 -0
- package/README.md +8 -2
- package/bin/_cctally_alerts.py +13 -2
- package/bin/_cctally_cache.py +3 -1
- package/bin/_cctally_cache_report.py +103 -6
- package/bin/_cctally_dashboard.py +1140 -282
- package/bin/_cctally_dashboard_conversation.py +12 -0
- package/bin/_cctally_dashboard_envelope.py +53 -61
- package/bin/_cctally_dashboard_share.py +101 -29
- package/bin/_cctally_dashboard_sources.py +663 -192
- package/bin/_cctally_diagnosis.py +1172 -0
- package/bin/_cctally_diagnosis_sources.py +4054 -0
- package/bin/_cctally_diff.py +20 -0
- package/bin/_cctally_forecast.py +329 -111
- package/bin/_cctally_milestone_history.py +10 -2
- package/bin/_cctally_parser.py +84 -0
- package/bin/_cctally_project.py +155 -47
- package/bin/_cctally_quota.py +14 -0
- package/bin/_cctally_record.py +151 -71
- package/bin/_cctally_refresh.py +105 -93
- package/bin/_cctally_share.py +9 -2
- package/bin/_cctally_source_analytics.py +40 -4
- package/bin/_cctally_statusline.py +8 -1
- package/bin/_cctally_tui.py +425 -234
- package/bin/_lib_alert_scope.py +685 -0
- package/bin/_lib_alerts_payload.py +112 -7
- package/bin/_lib_blocks.py +12 -0
- package/bin/_lib_cache_report.py +110 -1
- package/bin/_lib_codex_conversation.py +14 -0
- package/bin/_lib_codex_conversation_query.py +22 -8
- package/bin/_lib_codex_pools.py +20 -8
- package/bin/_lib_conversation.py +6 -3
- package/bin/_lib_conversation_query.py +256 -69
- package/bin/_lib_dashboard_sources.py +212 -24
- package/bin/_lib_diagnosis.py +1261 -0
- package/bin/_lib_forecast.py +62 -4
- package/bin/_lib_perf.py +12 -0
- package/bin/_lib_pricing.py +8 -7
- package/bin/_lib_readme_refresh.py +26 -5
- package/bin/_lib_render.py +31 -3
- package/bin/_lib_share_templates.py +150 -55
- package/bin/_lib_snapshot_cache.py +71 -13
- package/bin/_lib_source_identity.py +50 -2
- package/bin/_lib_subscription_weeks.py +65 -0
- package/bin/cctally +103 -16
- package/bin/cctally-explain +5 -0
- package/dashboard/static/assets/dashboardStream.shared-worker-1XTMV3nr.js +1 -0
- package/dashboard/static/assets/index-Di2hljvB.css +1 -0
- package/dashboard/static/assets/index-XYCIWjVG.js +97 -0
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +6 -1
- package/dashboard/static/assets/index-B5YfQEtn.css +0 -1
- package/dashboard/static/assets/index-Bt59nMMO.js +0 -97
|
@@ -101,7 +101,14 @@ CapabilityStatus = Literal[
|
|
|
101
101
|
# DESTRUCTIVE entry in this ledger rather than an additive one, and the null
|
|
102
102
|
# stub is what makes it survivable across an in-place `execvp` update. Removing
|
|
103
103
|
# the stub needs a separate change once no v9 bundle can be live.
|
|
104
|
-
|
|
104
|
+
# 10 -> 11 (#565): decorated providers now contribute certified per-account
|
|
105
|
+
# cycle legs to `sources.all.data.combined` instead of forcing
|
|
106
|
+
# `multi_account_unsupported`. Combined and leg `total_tokens` may be null when
|
|
107
|
+
# spend is certified but an equivalent account-scoped token operand is not;
|
|
108
|
+
# each leg gains `scope`, and account-cycle legs publish their exact account
|
|
109
|
+
# contribution periods. These fields change both shape and meaning, so the
|
|
110
|
+
# version moves even though undecorated arithmetic remains unchanged.
|
|
111
|
+
SOURCE_SCHEMA_VERSION = 11
|
|
105
112
|
DEFAULT_SOURCE = "claude"
|
|
106
113
|
SOURCE_ORDER = ("claude", "codex", "all")
|
|
107
114
|
SOURCE_FRESHNESS_DOMAINS = ("hero", "quota", "sessions")
|
|
@@ -212,6 +219,10 @@ class SourceDashboardState:
|
|
|
212
219
|
# would present as single-account and publish the one number §3.2 forbids,
|
|
213
220
|
# on precisely the install where it is wrong.
|
|
214
221
|
account_scope: Mapping[str, object] | None = None
|
|
222
|
+
# Server-only evidence used to compose decorated providers without
|
|
223
|
+
# re-deriving account truth from the public card shape. The carrier travels
|
|
224
|
+
# with the exact generation whose card operands it certifies.
|
|
225
|
+
combined_accounting: Mapping[str, object] | None = None
|
|
215
226
|
# Request-gated transcript content. This mapping is frozen with the source
|
|
216
227
|
# generation but is deliberately outside ``data``: source serialization
|
|
217
228
|
# publishes only ``data``, then the HTTP/SSE envelope layer injects a label
|
|
@@ -280,6 +291,10 @@ class SourceDashboardState:
|
|
|
280
291
|
object.__setattr__(self, "clock_data", _freeze(self.clock_data))
|
|
281
292
|
if self.account_scope is not None:
|
|
282
293
|
object.__setattr__(self, "account_scope", _freeze(self.account_scope))
|
|
294
|
+
if self.combined_accounting is not None:
|
|
295
|
+
object.__setattr__(
|
|
296
|
+
self, "combined_accounting", _freeze(self.combined_accounting),
|
|
297
|
+
)
|
|
283
298
|
if self.aggregate_scope is not None:
|
|
284
299
|
object.__setattr__(
|
|
285
300
|
self, "aggregate_scope", _freeze(self.aggregate_scope),
|
|
@@ -402,6 +417,7 @@ def degrade_source_state(
|
|
|
402
417
|
# it here would turn a transient provider failure into
|
|
403
418
|
# `account_scope_unresolved` on an install whose count read fine.
|
|
404
419
|
account_scope=prior.account_scope,
|
|
420
|
+
combined_accounting=prior.combined_accounting,
|
|
405
421
|
private_session_labels=prior.private_session_labels,
|
|
406
422
|
# #556 S2 §3.6: the carrier travels with the rows it describes. A
|
|
407
423
|
# degraded generation retains `prior.data`, so it must retain the range
|
|
@@ -520,12 +536,12 @@ def _cause_message(cause: _CombinedCause) -> str:
|
|
|
520
536
|
f"{provider}'s account count could not be read, so a combined "
|
|
521
537
|
"total is withheld."
|
|
522
538
|
)
|
|
523
|
-
if cause.code == "
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
539
|
+
if cause.code == "account_cost_unresolved":
|
|
540
|
+
return f"{provider} account spend could not be certified."
|
|
541
|
+
if cause.code == "account_cycle_unresolved":
|
|
542
|
+
return f"{provider} account cycle bounds could not be certified."
|
|
543
|
+
if cause.code == "account_reconciliation_failed":
|
|
544
|
+
return f"{provider} account cards did not reconcile with accounting."
|
|
529
545
|
if cause.code == "claude_cycle_unresolved":
|
|
530
546
|
return "Claude's current subscription week could not be resolved."
|
|
531
547
|
if cause.code == "codex_projection_incoherent":
|
|
@@ -604,6 +620,149 @@ def _period_instant(value: object) -> str | None:
|
|
|
604
620
|
return parsed.astimezone(dt.timezone.utc).isoformat().replace("+00:00", "Z")
|
|
605
621
|
|
|
606
622
|
|
|
623
|
+
def _aware_period_instant(value: object) -> str | None:
|
|
624
|
+
"""Canonicalize an explicitly aware bound; naive values fail closed."""
|
|
625
|
+
if not isinstance(value, str) or not value:
|
|
626
|
+
return None
|
|
627
|
+
try:
|
|
628
|
+
parsed = dt.datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
629
|
+
except ValueError:
|
|
630
|
+
return None
|
|
631
|
+
if parsed.tzinfo is None or parsed.utcoffset() is None:
|
|
632
|
+
return None
|
|
633
|
+
return parsed.astimezone(dt.timezone.utc).isoformat().replace("+00:00", "Z")
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
_ACCOUNT_PERIOD_KINDS: Mapping[PhysicalSource, frozenset[str]] = MappingProxyType({
|
|
637
|
+
"claude": frozenset({"subscription_week"}),
|
|
638
|
+
"codex": frozenset({"native_7_day_cycle", "trailing_7_day_cycle"}),
|
|
639
|
+
})
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
def _decorated_combined_leg(
|
|
643
|
+
state: SourceDashboardState,
|
|
644
|
+
provider: PhysicalSource,
|
|
645
|
+
) -> tuple[Mapping[str, object] | None, tuple[_CombinedCause, ...]]:
|
|
646
|
+
"""Validate a decorated provider's frozen card certificate at composition."""
|
|
647
|
+
carrier = state.combined_accounting
|
|
648
|
+
if not isinstance(carrier, Mapping):
|
|
649
|
+
return None, (_CombinedCause(
|
|
650
|
+
5, provider, "account_reconciliation_failed"),)
|
|
651
|
+
if carrier.get("scope") != "account_cycles":
|
|
652
|
+
return None, (_CombinedCause(
|
|
653
|
+
5, provider, "account_reconciliation_failed"),)
|
|
654
|
+
status = carrier.get("status")
|
|
655
|
+
if status == "unresolved":
|
|
656
|
+
cause = carrier.get("cause")
|
|
657
|
+
if cause == "account_cost_unresolved":
|
|
658
|
+
return None, (_CombinedCause(3, provider, cause),)
|
|
659
|
+
if cause == "account_cycle_unresolved":
|
|
660
|
+
return None, (_CombinedCause(4, provider, cause),)
|
|
661
|
+
return None, (_CombinedCause(
|
|
662
|
+
5, provider, "account_reconciliation_failed"),)
|
|
663
|
+
if status != "certified":
|
|
664
|
+
return None, (_CombinedCause(
|
|
665
|
+
5, provider, "account_reconciliation_failed"),)
|
|
666
|
+
|
|
667
|
+
data = state.data
|
|
668
|
+
cards = data.get("accounts") if isinstance(data, Mapping) else None
|
|
669
|
+
contributions = carrier.get("contributions")
|
|
670
|
+
if not isinstance(cards, (tuple, list)) or not isinstance(
|
|
671
|
+
contributions, (tuple, list)
|
|
672
|
+
):
|
|
673
|
+
return None, (_CombinedCause(
|
|
674
|
+
5, provider, "account_reconciliation_failed"),)
|
|
675
|
+
|
|
676
|
+
card_rows: list[tuple[str, float]] = []
|
|
677
|
+
for card in cards:
|
|
678
|
+
if not isinstance(card, Mapping):
|
|
679
|
+
return None, (_CombinedCause(
|
|
680
|
+
5, provider, "account_reconciliation_failed"),)
|
|
681
|
+
key = card.get("accountKey")
|
|
682
|
+
cost = card.get("spendUsd")
|
|
683
|
+
if (
|
|
684
|
+
not isinstance(key, str) or not key
|
|
685
|
+
or _counter_reason(cost, integral=False) is not None
|
|
686
|
+
):
|
|
687
|
+
return None, (_CombinedCause(
|
|
688
|
+
5, provider, "account_reconciliation_failed"),)
|
|
689
|
+
card_rows.append((key, float(cost)))
|
|
690
|
+
card_keys = [key for key, _cost in card_rows]
|
|
691
|
+
if len(card_keys) != len(set(card_keys)):
|
|
692
|
+
return None, (_CombinedCause(
|
|
693
|
+
5, provider, "account_reconciliation_failed"),)
|
|
694
|
+
|
|
695
|
+
public_rows: list[dict[str, object]] = []
|
|
696
|
+
contribution_keys: list[str] = []
|
|
697
|
+
for index, contribution in enumerate(contributions):
|
|
698
|
+
if not isinstance(contribution, Mapping):
|
|
699
|
+
return None, (_CombinedCause(
|
|
700
|
+
5, provider, "account_reconciliation_failed"),)
|
|
701
|
+
key = contribution.get("account_key")
|
|
702
|
+
cost = contribution.get("cost_usd")
|
|
703
|
+
if (
|
|
704
|
+
not isinstance(key, str) or not key
|
|
705
|
+
or _counter_reason(cost, integral=False) is not None
|
|
706
|
+
):
|
|
707
|
+
return None, (_CombinedCause(
|
|
708
|
+
5, provider, "account_reconciliation_failed"),)
|
|
709
|
+
contribution_keys.append(key)
|
|
710
|
+
period = contribution.get("period")
|
|
711
|
+
if not isinstance(period, Mapping):
|
|
712
|
+
return None, (_CombinedCause(4, provider, "account_cycle_unresolved"),)
|
|
713
|
+
kind = period.get("kind")
|
|
714
|
+
start_at = _aware_period_instant(period.get("start_at"))
|
|
715
|
+
end_at = _aware_period_instant(period.get("end_at"))
|
|
716
|
+
start_instant = (
|
|
717
|
+
dt.datetime.fromisoformat(start_at.replace("Z", "+00:00"))
|
|
718
|
+
if start_at is not None else None
|
|
719
|
+
)
|
|
720
|
+
end_instant = (
|
|
721
|
+
dt.datetime.fromisoformat(end_at.replace("Z", "+00:00"))
|
|
722
|
+
if end_at is not None else None
|
|
723
|
+
)
|
|
724
|
+
if (
|
|
725
|
+
kind not in _ACCOUNT_PERIOD_KINDS[provider]
|
|
726
|
+
or start_instant is None or end_instant is None
|
|
727
|
+
or start_instant >= end_instant
|
|
728
|
+
):
|
|
729
|
+
return None, (_CombinedCause(4, provider, "account_cycle_unresolved"),)
|
|
730
|
+
if index >= len(card_rows):
|
|
731
|
+
return None, (_CombinedCause(
|
|
732
|
+
5, provider, "account_reconciliation_failed"),)
|
|
733
|
+
card_key, card_cost = card_rows[index]
|
|
734
|
+
if key != card_key or abs(float(cost) - card_cost) > 1e-9:
|
|
735
|
+
return None, (_CombinedCause(
|
|
736
|
+
5, provider, "account_reconciliation_failed"),)
|
|
737
|
+
public_rows.append({
|
|
738
|
+
"account_key": key,
|
|
739
|
+
"cost_usd": card_cost,
|
|
740
|
+
"period": {
|
|
741
|
+
"kind": kind, "start_at": start_at, "end_at": end_at,
|
|
742
|
+
},
|
|
743
|
+
})
|
|
744
|
+
if contribution_keys != card_keys:
|
|
745
|
+
return None, (_CombinedCause(
|
|
746
|
+
5, provider, "account_reconciliation_failed"),)
|
|
747
|
+
|
|
748
|
+
total = math.fsum(cost for _key, cost in card_rows)
|
|
749
|
+
hero = _combined_hero(state)
|
|
750
|
+
hero_cost = hero.get("cost_usd") if hero is not None else None
|
|
751
|
+
if (
|
|
752
|
+
_counter_reason(hero_cost, integral=False) is not None
|
|
753
|
+
or abs(float(hero_cost) - total) > 1e-9
|
|
754
|
+
):
|
|
755
|
+
return None, (_CombinedCause(
|
|
756
|
+
5, provider, "account_reconciliation_failed"),)
|
|
757
|
+
return {
|
|
758
|
+
"state": "current",
|
|
759
|
+
"scope": "account_cycles",
|
|
760
|
+
"cost_usd": total,
|
|
761
|
+
"total_tokens": None,
|
|
762
|
+
"accounts": tuple(public_rows),
|
|
763
|
+
}, ()
|
|
764
|
+
|
|
765
|
+
|
|
607
766
|
def canonical_alerted_at(value: object) -> str:
|
|
608
767
|
"""Normalize an aware ISO-8601 firing instant to one UTC ``Z`` spelling.
|
|
609
768
|
|
|
@@ -710,7 +869,7 @@ def _combined_leg(
|
|
|
710
869
|
else ("claude_cycle_unresolved",)
|
|
711
870
|
)
|
|
712
871
|
return None, tuple(
|
|
713
|
-
_CombinedCause(
|
|
872
|
+
_CombinedCause(6, provider, code) for code in codes
|
|
714
873
|
)
|
|
715
874
|
cost = hero.get("cost_usd") if hero is not None else None
|
|
716
875
|
tokens = hero.get("total_tokens") if hero is not None else None
|
|
@@ -718,15 +877,18 @@ def _combined_leg(
|
|
|
718
877
|
if period is None and state.availability == "empty":
|
|
719
878
|
# Numeric zeros and no period, so nothing presents `$0` as observed
|
|
720
879
|
# spend inside a named cycle.
|
|
721
|
-
return {
|
|
880
|
+
return {
|
|
881
|
+
"state": "empty", "scope": "provider_cycle",
|
|
882
|
+
"cost_usd": 0.0, "total_tokens": 0,
|
|
883
|
+
}, ()
|
|
722
884
|
if provider == "claude" and cost is None and tokens is None:
|
|
723
885
|
# Accounting exists but no subscription week resolved. Claude's hero
|
|
724
886
|
# capability stays `supported` in that state, so this is its own
|
|
725
887
|
# detection rather than the capability branch above.
|
|
726
|
-
return None, (_CombinedCause(
|
|
888
|
+
return None, (_CombinedCause(6, provider, "claude_cycle_unresolved"),)
|
|
727
889
|
causes = tuple(
|
|
728
890
|
_CombinedCause(
|
|
729
|
-
|
|
891
|
+
7, provider, "invalid_counter", {"field": field, "reason": reason},
|
|
730
892
|
)
|
|
731
893
|
for field, reason in (
|
|
732
894
|
("cost_usd", _counter_reason(cost, integral=False)),
|
|
@@ -738,6 +900,7 @@ def _combined_leg(
|
|
|
738
900
|
return None, causes
|
|
739
901
|
return {
|
|
740
902
|
"state": "current",
|
|
903
|
+
"scope": "provider_cycle",
|
|
741
904
|
"cost_usd": float(cost),
|
|
742
905
|
"total_tokens": int(tokens),
|
|
743
906
|
**({"period": period} if period is not None else {}),
|
|
@@ -782,11 +945,10 @@ def _combined_outcome(
|
|
|
782
945
|
) -> tuple[Mapping[str, object] | None, Mapping[str, object] | None]:
|
|
783
946
|
"""Return ``(combined, combined_unavailable)`` — exactly one is not None.
|
|
784
947
|
|
|
785
|
-
Cause precedence (§
|
|
786
|
-
precedence: provider incoherence, unresolved account scope,
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
that `causes[0]` is always the winner.
|
|
948
|
+
Cause precedence (§6.5), first match wins, Claude before Codex at equal
|
|
949
|
+
precedence: provider incoherence, unresolved account scope, unresolved
|
|
950
|
+
account spend, unresolved account periods, reconciliation, provider-cycle
|
|
951
|
+
capability, then invalid counters.
|
|
790
952
|
"""
|
|
791
953
|
pairs: tuple[tuple[PhysicalSource, SourceDashboardState], ...] = (
|
|
792
954
|
("claude", claude), ("codex", codex),
|
|
@@ -800,18 +962,18 @@ def _combined_outcome(
|
|
|
800
962
|
if count is None:
|
|
801
963
|
causes.append(
|
|
802
964
|
_CombinedCause(2, provider, "account_scope_unresolved"))
|
|
803
|
-
elif count > 1:
|
|
804
|
-
causes.append(_CombinedCause(
|
|
805
|
-
3, provider, "multi_account_unsupported",
|
|
806
|
-
{"account_count": count},
|
|
807
|
-
))
|
|
808
965
|
legs: dict[str, Mapping[str, object]] = {}
|
|
809
966
|
for provider, state in pairs:
|
|
810
967
|
if not _coherent_provider(state):
|
|
811
968
|
# An incoherent generation's data cannot be trusted to yield a leg
|
|
812
969
|
# OR a leg-level cause; precedence 1 already withholds the figure.
|
|
813
970
|
continue
|
|
814
|
-
|
|
971
|
+
count = _real_account_count(state)
|
|
972
|
+
leg, leg_causes = (
|
|
973
|
+
_decorated_combined_leg(state, provider)
|
|
974
|
+
if count is not None and count > 1
|
|
975
|
+
else _combined_leg(state, provider)
|
|
976
|
+
)
|
|
815
977
|
causes.extend(leg_causes)
|
|
816
978
|
if leg is not None:
|
|
817
979
|
legs[provider] = leg
|
|
@@ -835,9 +997,15 @@ def _combined_outcome(
|
|
|
835
997
|
),
|
|
836
998
|
}
|
|
837
999
|
qualifications = _combined_qualifications(legs, claude, codex)
|
|
1000
|
+
token_values = tuple(legs[provider]["total_tokens"] for provider in ("claude", "codex"))
|
|
838
1001
|
return {
|
|
839
|
-
"cost_usd":
|
|
840
|
-
|
|
1002
|
+
"cost_usd": math.fsum(
|
|
1003
|
+
float(legs[provider]["cost_usd"]) for provider in ("claude", "codex")
|
|
1004
|
+
),
|
|
1005
|
+
"total_tokens": (
|
|
1006
|
+
None if any(value is None for value in token_values)
|
|
1007
|
+
else sum(int(value) for value in token_values)
|
|
1008
|
+
),
|
|
841
1009
|
"legs": legs,
|
|
842
1010
|
**({"qualifications": qualifications} if qualifications else {}),
|
|
843
1011
|
}, None
|
|
@@ -980,6 +1148,26 @@ def aggregate_scope_identity(scope: object) -> str:
|
|
|
980
1148
|
return "|".join(parts)
|
|
981
1149
|
|
|
982
1150
|
|
|
1151
|
+
def combined_accounting_version(
|
|
1152
|
+
base_version: str, carrier: Mapping[str, object] | None,
|
|
1153
|
+
) -> str:
|
|
1154
|
+
"""Bind a decorated source version to its frozen account certificate."""
|
|
1155
|
+
if carrier is None:
|
|
1156
|
+
return base_version
|
|
1157
|
+
|
|
1158
|
+
def _plain(value: object) -> object:
|
|
1159
|
+
if isinstance(value, Mapping):
|
|
1160
|
+
return {str(key): _plain(item) for key, item in value.items()}
|
|
1161
|
+
if isinstance(value, (tuple, list)):
|
|
1162
|
+
return [_plain(item) for item in value]
|
|
1163
|
+
return value
|
|
1164
|
+
|
|
1165
|
+
material = json.dumps(
|
|
1166
|
+
_plain(carrier), sort_keys=True, separators=(",", ":"),
|
|
1167
|
+
).encode("utf-8")
|
|
1168
|
+
return f"{base_version}:c{hashlib.sha256(material).hexdigest()[:16]}"
|
|
1169
|
+
|
|
1170
|
+
|
|
983
1171
|
def _aggregate_scope_range(state: SourceDashboardState) -> dict | None:
|
|
984
1172
|
scope = getattr(state, "aggregate_scope", None)
|
|
985
1173
|
if not isinstance(scope, Mapping):
|