cctally 1.97.0 → 1.99.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 +47 -0
- package/README.md +4 -4
- package/bin/_cctally_account.py +925 -0
- package/bin/_cctally_cache.py +829 -31
- package/bin/_cctally_core.py +52 -0
- package/bin/_cctally_dashboard.py +960 -55
- package/bin/_cctally_dashboard_envelope.py +43 -23
- package/bin/_cctally_dashboard_share.py +50 -1
- package/bin/_cctally_dashboard_sources.py +1580 -131
- package/bin/_cctally_db.py +810 -34
- package/bin/_cctally_doctor.py +184 -1
- package/bin/_cctally_journal.py +732 -76
- package/bin/_cctally_parser.py +65 -0
- package/bin/_cctally_quota.py +896 -17
- package/bin/_cctally_rederive.py +157 -5
- package/bin/_cctally_source_analytics.py +60 -6
- package/bin/_cctally_tui.py +811 -67
- package/bin/_lib_aggregators.py +117 -11
- package/bin/_lib_alert_axes.py +8 -3
- package/bin/_lib_budget.py +60 -0
- package/bin/_lib_codex_window_attribution.py +259 -0
- package/bin/_lib_dashboard_sources.py +488 -19
- package/bin/_lib_doctor.py +71 -0
- package/bin/_lib_journal.py +212 -0
- package/bin/_lib_jsonl.py +6 -0
- package/bin/_lib_rederive.py +8 -0
- package/bin/_lib_snapshot_cache.py +248 -7
- package/bin/_lib_source_analytics.py +20 -2
- package/bin/cctally +25 -0
- package/dashboard/static/assets/index-Bcbm-DNP.js +97 -0
- package/dashboard/static/assets/index-hJP4wlIO.css +1 -0
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +2 -1
- package/dashboard/static/assets/index-BSESoPIK.css +0 -1
- package/dashboard/static/assets/index-DgsMz5hA.js +0 -97
|
@@ -55,6 +55,8 @@ from _cctally_core import (
|
|
|
55
55
|
)
|
|
56
56
|
from _lib_dashboard_sources import (
|
|
57
57
|
SOURCE_SCHEMA_VERSION,
|
|
58
|
+
canonical_alerted_at as _canonical_alerted_at,
|
|
59
|
+
canonical_alerted_at_sql as _canonical_alerted_at_sql,
|
|
58
60
|
dashboard_resource_key as _dashboard_resource_key,
|
|
59
61
|
)
|
|
60
62
|
from _lib_display_tz import _compute_display_block, format_display_dt
|
|
@@ -361,6 +363,16 @@ def _alert_account_resolver(conn: sqlite3.Connection):
|
|
|
361
363
|
return fields
|
|
362
364
|
|
|
363
365
|
|
|
366
|
+
# #556 S3 §2.3. Every per-axis mapper orders by the CANONICAL firing instant
|
|
367
|
+
# before its ``LIMIT``, because that limit decides which rows exist downstream
|
|
368
|
+
# and a row excluded there is unrecoverable. Two spellings of one instant
|
|
369
|
+
# compare equal here; a nonzero offset — which the ``all-combined`` fixture
|
|
370
|
+
# stores deliberately — orders by its true instant rather than by its
|
|
371
|
+
# local-time text.
|
|
372
|
+
_CANON_ALERTED_AT = _canonical_alerted_at_sql()
|
|
373
|
+
_CANON_ALERTED_AT_M = _canonical_alerted_at_sql("m.alerted_at")
|
|
374
|
+
|
|
375
|
+
|
|
364
376
|
def _envelope_rows_weekly(
|
|
365
377
|
conn, descriptor, limit, severity_for, account_fields,
|
|
366
378
|
) -> list[dict]:
|
|
@@ -377,7 +389,7 @@ def _envelope_rows_weekly(
|
|
|
377
389
|
alerted_at, cumulative_cost_usd, reset_event_id, account_key
|
|
378
390
|
FROM {descriptor.milestone_table}
|
|
379
391
|
WHERE alerted_at IS NOT NULL
|
|
380
|
-
ORDER BY
|
|
392
|
+
ORDER BY {_CANON_ALERTED_AT} DESC
|
|
381
393
|
LIMIT ?
|
|
382
394
|
""",
|
|
383
395
|
(limit,),
|
|
@@ -431,7 +443,7 @@ def _envelope_rows_five_hour(
|
|
|
431
443
|
ON b.five_hour_window_key = m.five_hour_window_key
|
|
432
444
|
AND b.account_key = m.account_key
|
|
433
445
|
WHERE m.alerted_at IS NOT NULL
|
|
434
|
-
ORDER BY
|
|
446
|
+
ORDER BY {_CANON_ALERTED_AT_M} DESC
|
|
435
447
|
LIMIT ?
|
|
436
448
|
""",
|
|
437
449
|
(limit,),
|
|
@@ -506,7 +518,7 @@ def _envelope_rows_budget_family(
|
|
|
506
518
|
budget_usd, spent_usd, consumption_pct, account_key
|
|
507
519
|
FROM {descriptor.milestone_table}
|
|
508
520
|
WHERE vendor = ? AND alerted_at IS NOT NULL
|
|
509
|
-
ORDER BY
|
|
521
|
+
ORDER BY {_CANON_ALERTED_AT} DESC
|
|
510
522
|
LIMIT ?
|
|
511
523
|
""",
|
|
512
524
|
(default_noun, vendor, limit),
|
|
@@ -572,7 +584,7 @@ def _envelope_rows_projected(
|
|
|
572
584
|
denominator, crossed_at_utc, alerted_at, account_key
|
|
573
585
|
FROM {descriptor.milestone_table}
|
|
574
586
|
WHERE alerted_at IS NOT NULL
|
|
575
|
-
ORDER BY
|
|
587
|
+
ORDER BY {_CANON_ALERTED_AT} DESC
|
|
576
588
|
LIMIT ?
|
|
577
589
|
""",
|
|
578
590
|
(limit,),
|
|
@@ -629,7 +641,7 @@ def _envelope_rows_project_budget(
|
|
|
629
641
|
consumption_pct, crossed_at_utc, alerted_at, account_key
|
|
630
642
|
FROM {descriptor.milestone_table}
|
|
631
643
|
WHERE alerted_at IS NOT NULL
|
|
632
|
-
ORDER BY
|
|
644
|
+
ORDER BY {_CANON_ALERTED_AT} DESC
|
|
633
645
|
LIMIT ?
|
|
634
646
|
""",
|
|
635
647
|
(limit,),
|
|
@@ -858,11 +870,29 @@ def _build_alerts_envelope_array(
|
|
|
858
870
|
))
|
|
859
871
|
|
|
860
872
|
# Python's list.sort is stable. When two alerts share the same
|
|
861
|
-
# `alerted_at`
|
|
862
|
-
#
|
|
873
|
+
# `alerted_at` instant (rare; multiple axes firing within the same
|
|
874
|
+
# second), the union order (weekly, then 5h, then budget, then
|
|
863
875
|
# projected) determines the tiebreaker — no extra deterministic key is
|
|
864
876
|
# added because the spec doesn't require one.
|
|
865
|
-
|
|
877
|
+
#
|
|
878
|
+
# #556 S3 §2.3: the slice below is a truncation, so this re-sort compares
|
|
879
|
+
# instants rather than spellings. Each axis reaches here already ordered by
|
|
880
|
+
# its own canonical SQL expression, but the axes are merged raw, so two
|
|
881
|
+
# differently-spelled instants meet for the first time right here.
|
|
882
|
+
def _order_key(alert):
|
|
883
|
+
# A raise here empties the whole legacy array AND the Claude projection
|
|
884
|
+
# derived from it, so the diagnostic must name the offending row the way
|
|
885
|
+
# `_combined_alert_rows` does — otherwise the operator sees an empty
|
|
886
|
+
# panel and a message that identifies nothing.
|
|
887
|
+
try:
|
|
888
|
+
return _canonical_alerted_at(alert["alerted_at"])
|
|
889
|
+
except (KeyError, ValueError) as exc:
|
|
890
|
+
identity = alert.get("key") or alert.get("id")
|
|
891
|
+
raise ValueError(
|
|
892
|
+
f"alert row {identity!r} (axis {alert.get('axis')!r}): {exc}"
|
|
893
|
+
) from exc
|
|
894
|
+
|
|
895
|
+
out.sort(key=_order_key, reverse=True)
|
|
866
896
|
return out[:limit]
|
|
867
897
|
|
|
868
898
|
|
|
@@ -1270,21 +1300,11 @@ def snapshot_to_envelope(snap: "DataSnapshot", *,
|
|
|
1270
1300
|
}
|
|
1271
1301
|
|
|
1272
1302
|
def _daily_row_to_dict(r: "DailyPanelRow") -> dict:
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
"intensity_bucket": r.intensity_bucket,
|
|
1279
|
-
"models": list(r.models),
|
|
1280
|
-
# ---- v2.3 additions ----
|
|
1281
|
-
"input_tokens": r.input_tokens,
|
|
1282
|
-
"output_tokens": r.output_tokens,
|
|
1283
|
-
"cache_creation_tokens": r.cache_creation_tokens,
|
|
1284
|
-
"cache_read_tokens": r.cache_read_tokens,
|
|
1285
|
-
"total_tokens": r.total_tokens,
|
|
1286
|
-
"cache_hit_pct": r.cache_hit_pct,
|
|
1287
|
-
}
|
|
1303
|
+
# #556 S2 §6.3a: one owner for the daily row wire shape. The All-only
|
|
1304
|
+
# `periods.daily_aggregate.rows` sibling publishes the same shape, and
|
|
1305
|
+
# the client reads one shape whichever sibling produced it, so the
|
|
1306
|
+
# dict is built in `_cctally_dashboard` and this renderer delegates.
|
|
1307
|
+
return sys.modules["_cctally_dashboard"].daily_panel_row_to_wire(r)
|
|
1288
1308
|
|
|
1289
1309
|
# Spec §2.7: empty state is `weekly.rows === []`, not `weekly === null`.
|
|
1290
1310
|
# Always emit a `{rows: [...]}` envelope (possibly empty) so the panel
|
|
@@ -1662,7 +1662,8 @@ def _build_codex_source_share_snapshot(ls, *, state, panel: str,
|
|
|
1662
1662
|
ls.ColumnSpec(key="current", label="Current", align="right"),
|
|
1663
1663
|
ls.ColumnSpec(key="projected", label="Projected", align="right"),
|
|
1664
1664
|
),
|
|
1665
|
-
rows=tuple(rows), chart=None, totals=(),
|
|
1665
|
+
rows=tuple(rows), chart=None, totals=(),
|
|
1666
|
+
notes=_share_budget_notes(data), generated_at=end,
|
|
1666
1667
|
version=sys.modules["cctally"]._share_resolve_version(),
|
|
1667
1668
|
template_id=template_id, source="codex", source_label="Codex",
|
|
1668
1669
|
availability=availability, availability_reason=reason,
|
|
@@ -1979,6 +1980,38 @@ def _share_scope_codex_state(state, account: "str | None"):
|
|
|
1979
1980
|
return replace(state, data=MappingProxyType({**dict(data), **scoped}))
|
|
1980
1981
|
|
|
1981
1982
|
|
|
1983
|
+
def _share_budget_notes(data) -> tuple[str, ...]:
|
|
1984
|
+
"""The configured-budget status, as an artifact note (#556 S5 §5.12).
|
|
1985
|
+
|
|
1986
|
+
The Forecast artifact carried quota projections and nothing about the
|
|
1987
|
+
CONFIGURED budget, so a shared Forecast said less than the panel it was
|
|
1988
|
+
taken from — and after S5 the panel renders both side by side. The note is
|
|
1989
|
+
ADDITIVE and omitted when no status is published, so an install with no
|
|
1990
|
+
budget produces a byte-identical artifact.
|
|
1991
|
+
|
|
1992
|
+
``data`` is already the ACCOUNT-SCOPED provider body when the request named
|
|
1993
|
+
an account (`_share_scope_codex_state` rewrites the scoped
|
|
1994
|
+
domains before this runs), so a focused share carries that account's own
|
|
1995
|
+
budget and never the vendor-wide one.
|
|
1996
|
+
"""
|
|
1997
|
+
budget = data.get("budget") if isinstance(data, Mapping) else None
|
|
1998
|
+
status = budget.get("status") if isinstance(budget, Mapping) else None
|
|
1999
|
+
if not isinstance(status, Mapping):
|
|
2000
|
+
return ()
|
|
2001
|
+
try:
|
|
2002
|
+
spent = float(status["spent_usd"])
|
|
2003
|
+
target = float(status["budget_usd"])
|
|
2004
|
+
consumed = float(status["consumption_pct"])
|
|
2005
|
+
period = str(status["period"])
|
|
2006
|
+
verdict = str(status["verdict"])
|
|
2007
|
+
except (KeyError, TypeError, ValueError):
|
|
2008
|
+
return ()
|
|
2009
|
+
return (
|
|
2010
|
+
f"Budget ({period}): ${spent:,.2f} of ${target:,.2f} "
|
|
2011
|
+
f"({consumed:.1f}%) — {verdict}",
|
|
2012
|
+
)
|
|
2013
|
+
|
|
2014
|
+
|
|
1982
2015
|
def _share_build_source_snapshots(*, ls, template, template_id: str,
|
|
1983
2016
|
panel: str, options: dict, source: str,
|
|
1984
2017
|
source_explicit: bool, data_snap,
|
|
@@ -2010,6 +2043,22 @@ def _share_build_source_snapshots(*, ls, template, template_id: str,
|
|
|
2010
2043
|
claude_snapshot = _share_apply_current_week_freshness(
|
|
2011
2044
|
claude_snapshot, claude_state, panel,
|
|
2012
2045
|
)
|
|
2046
|
+
# #556 S5 §5.12 (Unit 2 review F7). The budget note was wired into
|
|
2047
|
+
# the Codex builder only, while §5.12 says "the configured-budget
|
|
2048
|
+
# sections" and the Claude Forecast panel renders one after S5 — so
|
|
2049
|
+
# a shared Claude Forecast said less than the panel it came from.
|
|
2050
|
+
# Gated exactly like the freshness stamp above: a source-less
|
|
2051
|
+
# request is the shipped legacy Claude contract and stays
|
|
2052
|
+
# byte-identical, because it has no source state to read at all.
|
|
2053
|
+
if panel == "forecast":
|
|
2054
|
+
notes = _share_budget_notes(
|
|
2055
|
+
getattr(claude_state, "data", None) or {},
|
|
2056
|
+
)
|
|
2057
|
+
if notes:
|
|
2058
|
+
claude_snapshot = replace(
|
|
2059
|
+
claude_snapshot,
|
|
2060
|
+
notes=tuple(claude_snapshot.notes) + notes,
|
|
2061
|
+
)
|
|
2013
2062
|
|
|
2014
2063
|
codex_snapshot = None
|
|
2015
2064
|
codex_state = None
|