cctally 1.103.0 → 1.104.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 +56 -0
- package/bin/_cctally_alerts.py +65 -4
- package/bin/_cctally_config.py +62 -2
- package/bin/_cctally_core.py +62 -1
- package/bin/_cctally_dashboard.py +11 -0
- package/bin/_cctally_dashboard_envelope.py +319 -14
- package/bin/_cctally_dashboard_share.py +56 -25
- package/bin/_cctally_doctor.py +63 -0
- package/bin/_cctally_forecast.py +917 -44
- package/bin/_cctally_journal.py +153 -5
- package/bin/_cctally_parser.py +66 -0
- package/bin/_cctally_project.py +535 -10
- package/bin/_cctally_quota.py +13 -0
- package/bin/_cctally_quota_calibration.py +146 -0
- package/bin/_cctally_quota_model.py +1616 -0
- package/bin/_cctally_record.py +114 -6
- package/bin/_cctally_share.py +16 -8
- package/bin/_cctally_statusline.py +34 -0
- package/bin/_cctally_tui.py +214 -45
- package/bin/_lib_dashboard_settings_contract.py +2 -0
- package/bin/_lib_doctor.py +159 -1
- package/bin/_lib_forecast.py +337 -43
- package/bin/_lib_meter_rate_change.py +294 -0
- package/bin/_lib_quota_calibration.py +311 -0
- package/bin/_lib_quota_copy.py +131 -0
- package/bin/_lib_quota_model.py +2333 -0
- package/bin/_lib_rederive.py +10 -0
- package/bin/_lib_render.py +6 -0
- package/bin/_lib_share_templates.py +37 -5
- package/bin/_lib_statusline.py +226 -2
- package/bin/_lib_view_models.py +30 -12
- package/bin/cctally +32 -0
- package/dashboard/static/assets/index-D19TO7Mg.js +97 -0
- package/dashboard/static/assets/index-klO46NcU.css +1 -0
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +7 -1
- package/dashboard/static/assets/index-Di2hljvB.css +0 -1
- package/dashboard/static/assets/index-XYCIWjVG.js +0 -97
package/bin/_cctally_tui.py
CHANGED
|
@@ -260,7 +260,10 @@ def _ensure_sibling_loaded(name: str) -> None:
|
|
|
260
260
|
|
|
261
261
|
|
|
262
262
|
_ensure_sibling_loaded("_lib_forecast")
|
|
263
|
-
from _lib_forecast import
|
|
263
|
+
from _lib_forecast import (
|
|
264
|
+
_compute_forecast, ForecastInputs, ForecastOutput, BudgetRow,
|
|
265
|
+
corrected_percent_point, projection_base,
|
|
266
|
+
)
|
|
264
267
|
_ensure_sibling_loaded("_lib_dashboard_sources")
|
|
265
268
|
_ensure_sibling_loaded("_cctally_dashboard_sources")
|
|
266
269
|
from _cctally_dashboard_sources import (
|
|
@@ -423,6 +426,11 @@ def _build_alerts_envelope_array(*args, **kwargs):
|
|
|
423
426
|
return sys.modules["cctally"]._build_alerts_envelope_array(*args, **kwargs)
|
|
424
427
|
|
|
425
428
|
|
|
429
|
+
def _build_meter_rate_change_array(*args, **kwargs):
|
|
430
|
+
return sys.modules["cctally"]._build_meter_rate_change_array(
|
|
431
|
+
*args, **kwargs)
|
|
432
|
+
|
|
433
|
+
|
|
426
434
|
def _select_current_block_for_envelope(*args, **kwargs):
|
|
427
435
|
return sys.modules["cctally"]._select_current_block_for_envelope(*args, **kwargs)
|
|
428
436
|
|
|
@@ -915,6 +923,12 @@ from _lib_view_models import ( # noqa: E402
|
|
|
915
923
|
TuiSessionRow,
|
|
916
924
|
)
|
|
917
925
|
|
|
926
|
+
# The ONE projection decomposition every consumer shares (#661 S2 spec
|
|
927
|
+
# section 3.3). The TUI used to re-derive `p_now + rate * remaining` at three
|
|
928
|
+
# render sites, which is how a censored reading reached the screen as a
|
|
929
|
+
# number the kernel had already withheld.
|
|
930
|
+
from _lib_view_models import _forecast_projection_pcts # noqa: E402
|
|
931
|
+
|
|
918
932
|
|
|
919
933
|
@dataclass
|
|
920
934
|
class TuiPercentMilestone:
|
|
@@ -1207,6 +1221,17 @@ class DataSnapshot:
|
|
|
1207
1221
|
# `current_week.five_hour_block` is precomputed via
|
|
1208
1222
|
# `_select_current_block_for_envelope`).
|
|
1209
1223
|
alerts: list[dict] = field(default_factory=list)
|
|
1224
|
+
# #661 S2 §6.1: the non-threshold rate-change family's wire rows, built
|
|
1225
|
+
# beside `alerts` at sync time for the same reason — the envelope stays a
|
|
1226
|
+
# pure renderer.
|
|
1227
|
+
meter_rate_changes: list[dict] = field(default_factory=list)
|
|
1228
|
+
# #661 S2 §6.6: the DERIVED marker state — the active open regime has a
|
|
1229
|
+
# confirmed predecessor — read from the calibration file at sync time so
|
|
1230
|
+
# the envelope stays a pure renderer. `None` means no change is active,
|
|
1231
|
+
# which is also what an absent, malformed or quarantined calibration
|
|
1232
|
+
# renders. Distinct from `meter_rate_changes` above, which is the event
|
|
1233
|
+
# HISTORY; this is whether the current regime is a successor.
|
|
1234
|
+
quota_rate_change: "dict | None" = None
|
|
1210
1235
|
# ---- 5h in-place credit (v1.7.x) ----
|
|
1211
1236
|
# Already-envelope-shaped dicts for the CurrentWeekModal's new 5h
|
|
1212
1237
|
# milestone timeline (spec §5.3, Codex r1 finding 3). Parallel to
|
|
@@ -1393,7 +1418,8 @@ class DataSnapshot:
|
|
|
1393
1418
|
five_hour_resets_at=as_of.replace(minute=0, second=0, microsecond=0)
|
|
1394
1419
|
+ dt.timedelta(hours=3),
|
|
1395
1420
|
spent_usd=spent_usd,
|
|
1396
|
-
|
|
1421
|
+
# The same corrected divisor `_tui_build_current_week` now uses.
|
|
1422
|
+
dollars_per_percent=spent_usd / corrected_percent_point(used_pct),
|
|
1397
1423
|
latest_snapshot_at=as_of,
|
|
1398
1424
|
freshness_label="fresh",
|
|
1399
1425
|
freshness_age=12,
|
|
@@ -1416,25 +1442,31 @@ class DataSnapshot:
|
|
|
1416
1442
|
# Recent 24h slightly lower: ~94% → r_recent = (94 - 53) / 82 ≈ 0.500.
|
|
1417
1443
|
r_recent = (94.0 - used_pct) / remaining_hours if remaining_hours > 0 else 0.0
|
|
1418
1444
|
p_24h_ago = max(0.0, used_pct - r_recent * 24.0)
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1445
|
+
# #661 S2. The CEILING-CORRECTED reading is the operand every render
|
|
1446
|
+
# path projects from, so the demo derives its projections and budget
|
|
1447
|
+
# headroom from the same one. Deriving them from the displayed
|
|
1448
|
+
# `used_pct` while `_tui_panel_forecast` reads the corrected base made
|
|
1449
|
+
# the demo screenshot's panel and header disagree by a point.
|
|
1450
|
+
base = corrected_percent_point(used_pct)
|
|
1451
|
+
dpp = spent_usd / base
|
|
1452
|
+
final_low = base + r_recent * remaining_hours
|
|
1453
|
+
final_high = base + r_avg * remaining_hours
|
|
1422
1454
|
# Two BudgetRows mirroring the TUI's hard-coded targets [100, 90].
|
|
1423
1455
|
budgets = [
|
|
1424
1456
|
BudgetRow(
|
|
1425
1457
|
target_percent=100,
|
|
1426
|
-
pct_headroom=100.0 -
|
|
1427
|
-
dollars_per_day=((100.0 -
|
|
1458
|
+
pct_headroom=100.0 - base,
|
|
1459
|
+
dollars_per_day=((100.0 - base) * dpp / remaining_days)
|
|
1428
1460
|
if remaining_days > 0 else None,
|
|
1429
|
-
percent_per_day=((100.0 -
|
|
1461
|
+
percent_per_day=((100.0 - base) / remaining_days)
|
|
1430
1462
|
if remaining_days > 0 else None,
|
|
1431
1463
|
),
|
|
1432
1464
|
BudgetRow(
|
|
1433
1465
|
target_percent=90,
|
|
1434
|
-
pct_headroom=90.0 -
|
|
1435
|
-
dollars_per_day=((90.0 -
|
|
1466
|
+
pct_headroom=90.0 - base,
|
|
1467
|
+
dollars_per_day=((90.0 - base) * dpp / remaining_days)
|
|
1436
1468
|
if remaining_days > 0 else None,
|
|
1437
|
-
percent_per_day=((90.0 -
|
|
1469
|
+
percent_per_day=((90.0 - base) / remaining_days)
|
|
1438
1470
|
if remaining_days > 0 else None,
|
|
1439
1471
|
),
|
|
1440
1472
|
]
|
|
@@ -1464,7 +1496,7 @@ class DataSnapshot:
|
|
|
1464
1496
|
r_recent=r_recent,
|
|
1465
1497
|
final_percent_low=final_low,
|
|
1466
1498
|
final_percent_high=final_high,
|
|
1467
|
-
week_avg_projection_pct=
|
|
1499
|
+
week_avg_projection_pct=base + r_avg * remaining_hours,
|
|
1468
1500
|
projected_cap=final_high >= 100.0,
|
|
1469
1501
|
already_capped=False,
|
|
1470
1502
|
cap_at=None,
|
|
@@ -1663,7 +1695,17 @@ def _tui_build_current_week(
|
|
|
1663
1695
|
spent, total_tokens = _sum_cost_and_tokens_for_range(
|
|
1664
1696
|
week_start_at, now_utc, mode="auto", skip_sync=skip_sync
|
|
1665
1697
|
)
|
|
1666
|
-
|
|
1698
|
+
# #661 S2 spec section 3.4. This card computes its own `spent / used_pct`
|
|
1699
|
+
# independently of the forecast kernel, so a correction confined to that
|
|
1700
|
+
# kernel would leave this surface stating an uncorrected figure. The GATE
|
|
1701
|
+
# stays on the displayed reading (a displayed 0 corrects to 0.25, and
|
|
1702
|
+
# dividing by that would publish a rate on a week with no observed
|
|
1703
|
+
# usage); only the DIVISOR is corrected. A right-censored reading has no
|
|
1704
|
+
# corrected point, so the displayed reading is retained as the divisor
|
|
1705
|
+
# there — the same operand this card has always used.
|
|
1706
|
+
used_pct_corrected = corrected_percent_point(used_pct)
|
|
1707
|
+
dpp_divisor = used_pct if used_pct_corrected is None else used_pct_corrected
|
|
1708
|
+
dpp = (spent / dpp_divisor) if used_pct > 0 else None
|
|
1667
1709
|
# Collect every textual variant of week_start_at that parses to the same
|
|
1668
1710
|
# instant — mirrors `_fetch_current_week_snapshots` lines 9199-9210 so
|
|
1669
1711
|
# legacy local-offset rows and newly UTC-canonicalized rows both contribute.
|
|
@@ -4384,6 +4426,8 @@ def _tui_build_snapshot_once(
|
|
|
4384
4426
|
blocks_panel: list[BlocksPanelRow] = []
|
|
4385
4427
|
daily_panel: list[DailyPanelRow] = []
|
|
4386
4428
|
alerts: list[dict] = []
|
|
4429
|
+
meter_rate_changes: list[dict] = []
|
|
4430
|
+
quota_rate_change: "dict | None" = None
|
|
4387
4431
|
# ── Sync-once (spec §4, #268) ──────────────────────────────────
|
|
4388
4432
|
# Ingest new JSONL bytes into the cache EXACTLY ONCE at the top of
|
|
4389
4433
|
# the rebuild, then read every builder with skip_sync=True (pure
|
|
@@ -4914,6 +4958,33 @@ def _tui_build_snapshot_once(
|
|
|
4914
4958
|
alerts = _build_alerts_envelope_array(conn)
|
|
4915
4959
|
except Exception as exc:
|
|
4916
4960
|
capture_failure("alerts", "stats", exc)
|
|
4961
|
+
# Its OWN phase, not `build.alerts` (#661 S2 Stage C review, F13).
|
|
4962
|
+
# `cctally dashboard-perf --trace` reads these names as the
|
|
4963
|
+
# attribution of where a tick's cost went, so a second query billed to
|
|
4964
|
+
# a phase that did not run it makes that attribution wrong — however
|
|
4965
|
+
# small the query is. §6.1 keeps this family out of `AXIS_REGISTRY`
|
|
4966
|
+
# precisely because it is not one of the threshold axes, and the trace
|
|
4967
|
+
# names follow the same separation.
|
|
4968
|
+
with _perf.phase("build.meter_rate_changes"):
|
|
4969
|
+
try:
|
|
4970
|
+
meter_rate_changes = _build_meter_rate_change_array(conn)
|
|
4971
|
+
except Exception as exc:
|
|
4972
|
+
capture_failure("meter_rate_changes", "stats", exc)
|
|
4973
|
+
try:
|
|
4974
|
+
# §6.6's predicate, from the calibration FILE. One small
|
|
4975
|
+
# read through the non-mutating reader — never
|
|
4976
|
+
# `load_calibrations`, which quarantines by renaming — and
|
|
4977
|
+
# no store at all. An unreadable file yields no marker,
|
|
4978
|
+
# which is the same thing an absent one yields, because the
|
|
4979
|
+
# reader cannot tell them apart without scanning sidecars.
|
|
4980
|
+
_mrc = _cctally()._load_sibling("_lib_meter_rate_change")
|
|
4981
|
+
_qg = _cctally()._load_sibling("_cctally_quota_model")
|
|
4982
|
+
_state = _qg.read_stored_state_readonly()
|
|
4983
|
+
if _state is not None:
|
|
4984
|
+
quota_rate_change = _mrc.active_rate_change(
|
|
4985
|
+
_qg.stored_regimes(_state, None))
|
|
4986
|
+
except Exception as exc:
|
|
4987
|
+
capture_failure("quota_rate_change", "calibration", exc)
|
|
4917
4988
|
# ---- 5h in-place credit (v1.7.x) ----
|
|
4918
4989
|
# Load 5h milestones (pre + post credit) for the current
|
|
4919
4990
|
# block's window so CurrentWeekModal can render a merged
|
|
@@ -5140,6 +5211,8 @@ def _tui_build_snapshot_once(
|
|
|
5140
5211
|
blocks_panel=blocks_panel,
|
|
5141
5212
|
daily_panel=daily_panel,
|
|
5142
5213
|
alerts=alerts,
|
|
5214
|
+
meter_rate_changes=meter_rate_changes,
|
|
5215
|
+
quota_rate_change=quota_rate_change,
|
|
5143
5216
|
five_hour_milestones=fh_milestones,
|
|
5144
5217
|
week_index=week_index,
|
|
5145
5218
|
daily_total_cost_usd=daily_total_cost_usd,
|
|
@@ -6275,6 +6348,10 @@ def _tui_verdict_of(forecast) -> str:
|
|
|
6275
6348
|
if forecast is None or getattr(forecast.inputs, "confidence", "high") == "low":
|
|
6276
6349
|
return "LOW CONF"
|
|
6277
6350
|
high = forecast.final_percent_high
|
|
6351
|
+
if high is None:
|
|
6352
|
+
# A right-censored reading publishes no projection, and the meter is
|
|
6353
|
+
# at its cap by construction (#661 S2 spec section 3.2).
|
|
6354
|
+
return "OVER"
|
|
6278
6355
|
if high >= 100:
|
|
6279
6356
|
return "OVER"
|
|
6280
6357
|
if high >= 90:
|
|
@@ -6282,6 +6359,41 @@ def _tui_verdict_of(forecast) -> str:
|
|
|
6282
6359
|
return "GOOD"
|
|
6283
6360
|
|
|
6284
6361
|
|
|
6362
|
+
#: How each withheld-projection cause reads in the TUI's short register.
|
|
6363
|
+
#: The codes are members of the quota kernel's closed `EVIDENCE_CODES` union
|
|
6364
|
+
#: and arrive on `ForecastOutput.projection_code`.
|
|
6365
|
+
#: The SHORT register of spec section 8: every TUI slot that can carry a
|
|
6366
|
+
#: withheld figure is width-fixed, so the sentence form would wrap outside the
|
|
6367
|
+
#: panel border. The full sentences belong to the wide surfaces.
|
|
6368
|
+
_TUI_PROJECTION_CAUSE: dict = {
|
|
6369
|
+
"right-censored": "meter at its cap — no upper bound",
|
|
6370
|
+
"no-local-history": "no usage observed this week",
|
|
6371
|
+
}
|
|
6372
|
+
|
|
6373
|
+
|
|
6374
|
+
def _tui_projection_cause(forecast) -> str:
|
|
6375
|
+
"""The short token a withheld projection states, never a blank."""
|
|
6376
|
+
code = getattr(forecast, "projection_code", None)
|
|
6377
|
+
return _TUI_PROJECTION_CAUSE.get(code, "projection unavailable")
|
|
6378
|
+
|
|
6379
|
+
|
|
6380
|
+
def _tui_projection_pair(forecast) -> "tuple[int | None, int | None]":
|
|
6381
|
+
"""`(week_avg_pct, recent_24h_pct)` as whole percents, or `(None, None)`.
|
|
6382
|
+
|
|
6383
|
+
The week-average value is the kernel's SELECTED projection rather than a
|
|
6384
|
+
re-derivation of it (#661 S2 spec section 3.3), so a calibrated basis
|
|
6385
|
+
reaches the screen as the model's number and a withheld one reaches it as
|
|
6386
|
+
nothing at all. The recent-24h value is the second method and stays
|
|
6387
|
+
derived from the corrected base; it collapses onto the first when there
|
|
6388
|
+
is no 24-hour-prior sample.
|
|
6389
|
+
"""
|
|
6390
|
+
week_avg, recent = _forecast_projection_pcts(forecast)
|
|
6391
|
+
if week_avg is None:
|
|
6392
|
+
return None, None
|
|
6393
|
+
return int(round(week_avg)), int(round(recent if recent is not None
|
|
6394
|
+
else week_avg))
|
|
6395
|
+
|
|
6396
|
+
|
|
6285
6397
|
def _tui_panel_forecast(
|
|
6286
6398
|
snap: DataSnapshot,
|
|
6287
6399
|
runtime: RuntimeState,
|
|
@@ -6308,13 +6420,11 @@ def _tui_panel_forecast(
|
|
|
6308
6420
|
cls = _tui_bar_color(val)
|
|
6309
6421
|
return f"{{{cls}}}{b}{{/}}"
|
|
6310
6422
|
|
|
6311
|
-
#
|
|
6312
|
-
#
|
|
6313
|
-
# swap labels when the recent-24h rate
|
|
6314
|
-
|
|
6315
|
-
|
|
6316
|
-
wa = int(round(p_now + fc.r_avg * remaining))
|
|
6317
|
-
rc = wa if fc.r_recent is None else int(round(p_now + fc.r_recent * remaining))
|
|
6423
|
+
# The kernel's SELECTED week-average projection, and the recent-24h
|
|
6424
|
+
# method beside it (#661 S2 spec section 3.3). Not final_low/final_high,
|
|
6425
|
+
# which are min/max aggregates and swap labels when the recent-24h rate
|
|
6426
|
+
# is lower than the week average.
|
|
6427
|
+
wa, rc = _tui_projection_pair(fc)
|
|
6318
6428
|
# Budget table row values
|
|
6319
6429
|
b100 = next((r for r in fc.budgets if r.target_percent == 100), None)
|
|
6320
6430
|
b90 = next((r for r in fc.budgets if r.target_percent == 90), None)
|
|
@@ -6322,12 +6432,25 @@ def _tui_panel_forecast(
|
|
|
6322
6432
|
b90_str = f"${b90.dollars_per_day:.2f}/day" if b90 and b90.dollars_per_day is not None else "—"
|
|
6323
6433
|
conf = "low" if verdict == "LOW CONF" else "high"
|
|
6324
6434
|
|
|
6435
|
+
if wa is None:
|
|
6436
|
+
# #661 S2 spec section 3.2: no projection is fabricated from a meter
|
|
6437
|
+
# that supplies no point estimate. The two bars are replaced by the
|
|
6438
|
+
# typed cause, not by a zero.
|
|
6439
|
+
projection_lines = [
|
|
6440
|
+
f" {{dim}}Projection{{/}} {{warn}}withheld{{/}}",
|
|
6441
|
+
f" {{faint}}{_tui_projection_cause(fc)}{{/}}",
|
|
6442
|
+
]
|
|
6443
|
+
else:
|
|
6444
|
+
projection_lines = [
|
|
6445
|
+
f" {{dim}}Projection by week-avg{{/}} {bar_tagged(wa)} {{bright}}{wa:>3d}%{{/}}",
|
|
6446
|
+
f" {{dim}}Projection by recent 24h{{/}} {bar_tagged(rc)} {{bright}}{rc:>3d}%{{/}}",
|
|
6447
|
+
]
|
|
6448
|
+
|
|
6325
6449
|
return [
|
|
6326
6450
|
"",
|
|
6327
6451
|
f" {{badge.{vcls}}} [ {verdict} ] {{/}} {{dim}}{vmsg}{{/}}",
|
|
6328
6452
|
"",
|
|
6329
|
-
|
|
6330
|
-
f" {{dim}}Projection by recent 24h{{/}} {bar_tagged(rc)} {{bright}}{rc:>3d}%{{/}}",
|
|
6453
|
+
] + projection_lines + [
|
|
6331
6454
|
"",
|
|
6332
6455
|
f" {{dim}}Budget to stay ≤100%{{/}} {{bright}}{b100_str}{{/}}",
|
|
6333
6456
|
f" {{dim}}Budget to stay ≤90%{{/}} {{bright}}{b90_str}{{/}}",
|
|
@@ -6759,7 +6882,8 @@ def _tui_header_strip_a(
|
|
|
6759
6882
|
# on final_percent_high). Using low here would display e.g.
|
|
6760
6883
|
# "Fcst 74% WARN" where the WARN comes from a >=90% high
|
|
6761
6884
|
# projection, understating risk in the most glanceable line.
|
|
6762
|
-
fcst_pct =
|
|
6885
|
+
fcst_pct = ("\u2014" if fc.final_percent_high is None
|
|
6886
|
+
else f"{int(round(fc.final_percent_high))}%")
|
|
6763
6887
|
hdr = preview_prefix + (
|
|
6764
6888
|
f"{{bright.b}}Week {format_display_dt(cw.week_start_at, runtime.display_tz, fmt='%b %d', suffix=False)}–{format_display_dt(cw.week_end_at, runtime.display_tz, fmt='%b %d', suffix=False)}{{/}} "
|
|
6765
6889
|
f"{{faint}}│{{/}} Used {{{used_cls}.b}}{cw.used_pct:.1f}%{{/}} "
|
|
@@ -7156,18 +7280,17 @@ def _tui_render_variant_b(
|
|
|
7156
7280
|
vcls = _TUI_VERDICT_CLS[verdict]
|
|
7157
7281
|
# _TUI_VERDICT_CLS always maps to ok/warn/bad after the SSoT fix, so vcls
|
|
7158
7282
|
# is guaranteed to be a valid badge class here.
|
|
7283
|
+
wa = rc = None
|
|
7159
7284
|
if snap.forecast:
|
|
7160
|
-
#
|
|
7161
|
-
#
|
|
7162
|
-
|
|
7163
|
-
p_now = snap.forecast.inputs.p_now
|
|
7164
|
-
remaining = snap.forecast.inputs.remaining_hours
|
|
7165
|
-
wa = int(round(p_now + snap.forecast.r_avg * remaining))
|
|
7166
|
-
rc = wa if snap.forecast.r_recent is None else int(round(p_now + snap.forecast.r_recent * remaining))
|
|
7167
|
-
else:
|
|
7168
|
-
wa, rc = 0, 0
|
|
7285
|
+
# The kernel's selected projection and the second method beside it,
|
|
7286
|
+
# through the one decomposition (#661 S2 spec section 3.3).
|
|
7287
|
+
wa, rc = _tui_projection_pair(snap.forecast)
|
|
7169
7288
|
vmsg = _TUI_VERDICT_SHORT[verdict]
|
|
7170
|
-
|
|
7289
|
+
if wa is None:
|
|
7290
|
+
ribbon_text = (f" [ {verdict} ] {vmsg} · "
|
|
7291
|
+
f"projection withheld")
|
|
7292
|
+
else:
|
|
7293
|
+
ribbon_text = f" [ {verdict} ] {vmsg} · week-avg {wa}% · recent-24h {rc}%"
|
|
7171
7294
|
ribbon_pad = max(0, width - len(ribbon_text))
|
|
7172
7295
|
ribbon = f"{{badge.{vcls}}}{ribbon_text}{' ' * ribbon_pad}{{/}}"
|
|
7173
7296
|
|
|
@@ -7267,11 +7390,21 @@ def _tui_render_variant_b(
|
|
|
7267
7390
|
format_display_dt(cw.week_end_at, runtime.display_tz, fmt="%b %d %H:%M", suffix=True)
|
|
7268
7391
|
if cw else "—"
|
|
7269
7392
|
)
|
|
7393
|
+
if wa is None:
|
|
7394
|
+
# #661 S2 spec section 3.2: the strip states the withholding and
|
|
7395
|
+
# its cause rather than colouring a bar from a meter that supplies
|
|
7396
|
+
# no point estimate.
|
|
7397
|
+
projection_fragment = (
|
|
7398
|
+
f" {{dim}}projection{{/}} {{warn.b}}withheld{{/}} "
|
|
7399
|
+
f"{{faint}}{_tui_projection_cause(snap.forecast)}{{/}} ")
|
|
7400
|
+
else:
|
|
7401
|
+
projection_fragment = (
|
|
7402
|
+
f" {{dim}}wk-avg{{/}} {{{_tui_bar_color(wa)}.b}}{wa}%{{/}} "
|
|
7403
|
+
f"{{dim}}24h{{/}} {{{_tui_bar_color(rc)}.b}}{rc}%{{/}} ")
|
|
7270
7404
|
fcstrip_body = [
|
|
7271
7405
|
"",
|
|
7272
|
-
(
|
|
7273
|
-
f"{{
|
|
7274
|
-
f"{{faint}}│{{/}} "
|
|
7406
|
+
(projection_fragment
|
|
7407
|
+
+ f"{{faint}}│{{/}} "
|
|
7275
7408
|
f"{{dim}}≤100%{{/}} {{bright.b}}{b100_s}{{/}} "
|
|
7276
7409
|
f"{{dim}}≤90%{{/}} {{bright.b}}{b90_s}{{/}} "
|
|
7277
7410
|
f"{{faint}}│{{/}} "
|
|
@@ -7633,24 +7766,60 @@ def _tui_modal_forecast(snap, runtime, width):
|
|
|
7633
7766
|
else:
|
|
7634
7767
|
lines.append(" {dim}Used 24h ago{/} {dim}— (insufficient history){/}")
|
|
7635
7768
|
lines.append("")
|
|
7636
|
-
|
|
7637
|
-
|
|
7638
|
-
|
|
7639
|
-
|
|
7769
|
+
# The hero band above prints the DISPLAYED readings, which is what every
|
|
7770
|
+
# other surface shows. The two rate lines below divide the CEILING-
|
|
7771
|
+
# CORRECTED midpoints of those readings, so the operands differ from the
|
|
7772
|
+
# numbers three lines up by half a point each. Saying so is the whole
|
|
7773
|
+
# point of an explain modal (#661 S2).
|
|
7774
|
+
lines.append(" {dim.b}Two rate paths{/} "
|
|
7775
|
+
"{faint}(operands are ceiling-corrected){/}")
|
|
7776
|
+
# `getattr`, because the modal is also driven by duck-typed inputs stubs
|
|
7777
|
+
# that predate the corrected pair; those fall back to the raw reading.
|
|
7778
|
+
_p_corrected = getattr(inp, "p_now_corrected", None)
|
|
7779
|
+
_p24_corrected = getattr(inp, "p_24h_ago_corrected", None)
|
|
7780
|
+
_rate_num = projection_base(inp)
|
|
7781
|
+
_censored = bool(getattr(fc, "right_censored", False))
|
|
7782
|
+
if fc.r_avg is None:
|
|
7783
|
+
lines.append(" {dim} r_avg unavailable — "
|
|
7784
|
+
+ _tui_projection_cause(fc) + "{/}")
|
|
7785
|
+
else:
|
|
7786
|
+
lines.append(f" {{dim}} r_avg {_rate_num:.1f} / {inp.elapsed_hours:.1f} = {{/}}{{b}}{fc.r_avg:.4f} %/h{{/}}")
|
|
7787
|
+
if (fc.r_recent is not None and _p24_corrected is not None
|
|
7788
|
+
and _p_corrected is not None):
|
|
7789
|
+
lines.append(f" {{dim}} r_recent ({_p_corrected:.1f}-{_p24_corrected:.1f}) / {inp.t_24h_actual_hours:.1f} = {{/}}{{b}}{fc.r_recent:.4f} %/h{{/}}")
|
|
7790
|
+
elif _censored:
|
|
7791
|
+
# The cause is the CENSORING, not a missing sample: this shape can
|
|
7792
|
+
# carry a 24-hour-prior reading and still have no rate, because the
|
|
7793
|
+
# current reading has no point estimate to subtract from.
|
|
7794
|
+
lines.append(" {dim} r_recent unavailable — "
|
|
7795
|
+
+ _tui_projection_cause(fc) + "{/}")
|
|
7640
7796
|
else:
|
|
7641
7797
|
lines.append(" {dim} r_recent unavailable — no 24h-prior sample{/}")
|
|
7642
7798
|
lines.append("")
|
|
7643
7799
|
lines.append(f" {{dim.b}}Project to week end ({inp.remaining_hours:.1f}h remaining){{/}}")
|
|
7644
|
-
|
|
7645
|
-
|
|
7646
|
-
|
|
7647
|
-
|
|
7800
|
+
_wa_sel = getattr(fc, "week_avg_projection_pct", None)
|
|
7801
|
+
if _wa_sel is None:
|
|
7802
|
+
lines.append(" {dim} projection = withheld — "
|
|
7803
|
+
+ _tui_projection_cause(fc) + "{/}")
|
|
7804
|
+
elif fc.r_recent is not None and fc.final_percent_high is not None:
|
|
7805
|
+
base = _rate_num
|
|
7806
|
+
rc = base + fc.r_recent * inp.remaining_hours
|
|
7807
|
+
lines.append(f" {{dim}} by week-avg = {{/}}{{warn}}{_wa_sel:.1f}%{{/}}")
|
|
7648
7808
|
lines.append(f" {{dim}} by recent-24h = {{/}}{{ok}}{rc:.1f}%{{/}}")
|
|
7649
7809
|
lines.append(f" {{dim}} high = {{/}}{{{vcls}.b}}{fc.final_percent_high:.1f}%{{/}} {{dim}}verdict:{{/}} {{{vcls}.b}}{verdict}{{/}}")
|
|
7650
7810
|
else:
|
|
7651
|
-
lines.append(f" {{dim}} projection = {{/}}{{{vcls}.b}}{
|
|
7811
|
+
lines.append(f" {{dim}} projection = {{/}}{{{vcls}.b}}{_wa_sel:.1f}%{{/}} {{dim}}verdict:{{/}} {{{vcls}.b}}{verdict}{{/}}")
|
|
7652
7812
|
lines.append("")
|
|
7653
7813
|
lines.append(f" {{dim.b}}Daily $ budgets ({inp.remaining_days:.3f} days remaining){{/}}")
|
|
7814
|
+
if not fc.budgets:
|
|
7815
|
+
# The censored branch of `_compute_forecast` publishes NO budget rows
|
|
7816
|
+
# at all, because every one of them is priced off headroom the meter
|
|
7817
|
+
# cannot bound. The loop below then emitted nothing and the section
|
|
7818
|
+
# rendered as a bare header with no rows and no cause — beside a
|
|
7819
|
+
# projection line that states its own. A heading over silence reads
|
|
7820
|
+
# as a rendering fault, not as a withholding.
|
|
7821
|
+
lines.append(" {dim} — withheld — " + _tui_projection_cause(fc)
|
|
7822
|
+
+ "{/}")
|
|
7654
7823
|
for b in fc.budgets:
|
|
7655
7824
|
if b.dollars_per_day is not None:
|
|
7656
7825
|
lines.append(f" {{dim}} ≤{b.target_percent}% {{/}}{{b}}${b.dollars_per_day:.2f}/day{{/}}")
|
|
@@ -25,6 +25,8 @@ SETTINGS_LEAF_DISPOSITIONS = {
|
|
|
25
25
|
"display.tz": WRITABLE,
|
|
26
26
|
"alerts.enabled": WRITABLE,
|
|
27
27
|
"alerts.projected_enabled": WRITABLE,
|
|
28
|
+
# #661 S2 section 6.2. Recording is unconditional; this gates the PUSH.
|
|
29
|
+
"alerts.rate_change_enabled": WRITABLE,
|
|
28
30
|
"alerts.notifier": WRITABLE,
|
|
29
31
|
"dashboard.cache_failure_markers": WRITABLE,
|
|
30
32
|
"dashboard.live_tail": WRITABLE,
|
package/bin/_lib_doctor.py
CHANGED
|
@@ -141,6 +141,23 @@ class DoctorState:
|
|
|
141
141
|
# token_total); the kernel only reads `.kind`/`.model`/`.entry_count`/
|
|
142
142
|
# `.token_total`, so any duck-typed equivalent works for tests.
|
|
143
143
|
pricing_coverage: Optional[list] = None
|
|
144
|
+
# #661 S2 §7. The `quota` category's whole input, gathered through the
|
|
145
|
+
# §1.1 NON-MUTATING reader rather than `load_calibrations`, which renames
|
|
146
|
+
# a malformed or version-ahead file aside through `_quarantine` and would
|
|
147
|
+
# make a documented read-only command a writer.
|
|
148
|
+
#
|
|
149
|
+
# `quota_calibration` is `{"rejection": str | None, "status": str | None,
|
|
150
|
+
# "present": bool}`; `None` means the gather could not run at all (an
|
|
151
|
+
# import failure or a hand-built state), which reads as OK-with-detail
|
|
152
|
+
# exactly as `pricing_coverage is None` does.
|
|
153
|
+
#
|
|
154
|
+
# `quota_rate_change` is `{"active": bool, "effective_from": str | None,
|
|
155
|
+
# "previous_units_per_point": float | None,
|
|
156
|
+
# "new_units_per_point": float | None}` — §6.6's DERIVED marker
|
|
157
|
+
# predicate, which is "the active open regime has a confirmed
|
|
158
|
+
# predecessor" and requires no new durable state.
|
|
159
|
+
quota_calibration: Optional[dict] = None
|
|
160
|
+
quota_rate_change: Optional[dict] = None
|
|
144
161
|
# Conversation viewer (Plan 2, spec §5): the resolved
|
|
145
162
|
# `dashboard.expose_transcripts` opt-in. Only consequential when the bind
|
|
146
163
|
# is LAN — `_check_safety_dashboard_bind` then surfaces an extra
|
|
@@ -952,7 +969,7 @@ def _check_db_version_ahead(s: DoctorState) -> CheckResult:
|
|
|
952
969
|
# current index as a mismatch; keep this in lockstep with core
|
|
953
970
|
# (#496 S5b §6.1). It stays a literal because this kernel is pure and
|
|
954
971
|
# must not import `_cctally_core`.
|
|
955
|
-
epoch =
|
|
972
|
+
epoch = 1011
|
|
956
973
|
mismatch = uv > legacy_head and uv != epoch
|
|
957
974
|
return {"user_version": uv, "legacy_head": legacy_head, "epoch": epoch,
|
|
958
975
|
"mismatch": mismatch}
|
|
@@ -1168,6 +1185,24 @@ def _check_data_codex_cache(s: DoctorState) -> CheckResult:
|
|
|
1168
1185
|
)
|
|
1169
1186
|
|
|
1170
1187
|
|
|
1188
|
+
def _utc_day_label(stamp: object) -> str:
|
|
1189
|
+
"""One ISO stamp as its UTC calendar day, or `""` when unusable.
|
|
1190
|
+
|
|
1191
|
+
A text slice is not this: `"2026-08-25T23:00:00+02:00"[:10]` is
|
|
1192
|
+
`2026-08-25` where the UTC day is the 25th only by luck of the offset,
|
|
1193
|
+
and the store holds mixed offset spellings.
|
|
1194
|
+
"""
|
|
1195
|
+
if not stamp:
|
|
1196
|
+
return ""
|
|
1197
|
+
try:
|
|
1198
|
+
parsed = dt.datetime.fromisoformat(str(stamp).replace("Z", "+00:00"))
|
|
1199
|
+
except (ValueError, TypeError):
|
|
1200
|
+
return ""
|
|
1201
|
+
if parsed.tzinfo is None:
|
|
1202
|
+
parsed = parsed.replace(tzinfo=dt.timezone.utc)
|
|
1203
|
+
return parsed.astimezone(dt.timezone.utc).strftime("%Y-%m-%d")
|
|
1204
|
+
|
|
1205
|
+
|
|
1171
1206
|
def _age_seconds(stamp: object, now_utc: dt.datetime) -> Optional[int]:
|
|
1172
1207
|
"""Seconds between an ISO-8601 UTC stamp and ``now_utc``; None if unusable."""
|
|
1173
1208
|
if not stamp:
|
|
@@ -3535,6 +3570,15 @@ _CATEGORY_DEFINITIONS: tuple[tuple[str, str, tuple[tuple[str, str], ...]], ...]
|
|
|
3535
3570
|
("pricing", "Pricing", (
|
|
3536
3571
|
("pricing.coverage", "_check_pricing_coverage"),
|
|
3537
3572
|
)),
|
|
3573
|
+
# #661 S2 §7. NEITHER check can FAIL, so `doctor`'s exit code is
|
|
3574
|
+
# unaffected — the same posture `pricing.coverage` and
|
|
3575
|
+
# `data.parse_health` take. A metering-rate change is the provider's
|
|
3576
|
+
# behaviour, not a cctally malfunction, and an absent calibration is the
|
|
3577
|
+
# ordinary state of an install that has never run `cctally quota`.
|
|
3578
|
+
("quota", "Quota", (
|
|
3579
|
+
("quota.meter_drift", "_check_quota_meter_drift"),
|
|
3580
|
+
("quota.calibration", "_check_quota_calibration"),
|
|
3581
|
+
)),
|
|
3538
3582
|
("safety", "Safety", (
|
|
3539
3583
|
("safety.dashboard_bind", "_check_safety_dashboard_bind"),
|
|
3540
3584
|
("safety.backup_sync", "_check_safety_backup_sync"),
|
|
@@ -3549,6 +3593,120 @@ _CATEGORY_DEFINITIONS: tuple[tuple[str, str, tuple[tuple[str, str], ...]], ...]
|
|
|
3549
3593
|
)
|
|
3550
3594
|
|
|
3551
3595
|
|
|
3596
|
+
#: Which `RegimeRejection` values are a WARNing rather than an ordinary
|
|
3597
|
+
#: state. §7: stale means a fingerprint or algorithm-revision mismatch, NOT
|
|
3598
|
+
#: calendar age — S1 defines no age — so a calibration fitted in January under
|
|
3599
|
+
#: the current constants is healthy and one fitted yesterday under other
|
|
3600
|
+
#: constants is not.
|
|
3601
|
+
_QUOTA_CALIBRATION_WARN: dict = {
|
|
3602
|
+
"calibration-schema-version":
|
|
3603
|
+
"the stored calibration is from a newer cctally",
|
|
3604
|
+
"calibration-fingerprint-mismatch":
|
|
3605
|
+
"the stored calibration was fitted under other constants",
|
|
3606
|
+
"calibration-revision-mismatch":
|
|
3607
|
+
"the stored calibration was fitted by an earlier algorithm",
|
|
3608
|
+
"calibration-malformed": "the stored calibration could not be parsed",
|
|
3609
|
+
"calibration-unreadable": "the stored calibration could not be read",
|
|
3610
|
+
}
|
|
3611
|
+
|
|
3612
|
+
#: The rejections that are ordinary rather than a problem. An
|
|
3613
|
+
#: already-quarantined file is reported simply as a missing primary: the
|
|
3614
|
+
#: reader cannot tell it from an absent one without scanning sidecars, and it
|
|
3615
|
+
#: does not scan them.
|
|
3616
|
+
_QUOTA_CALIBRATION_OK: dict = {
|
|
3617
|
+
"calibration-absent": "no calibration fitted yet",
|
|
3618
|
+
"calibration-detection-only":
|
|
3619
|
+
"a metering-rate change is fitted but not yet predictive",
|
|
3620
|
+
}
|
|
3621
|
+
|
|
3622
|
+
|
|
3623
|
+
def _check_quota_calibration(s: DoctorState) -> CheckResult:
|
|
3624
|
+
"""WARN on an unreadable, version-ahead or fingerprint-mismatched
|
|
3625
|
+
calibration; OK on an absent one or one with insufficient history.
|
|
3626
|
+
|
|
3627
|
+
It cannot FAIL. Every state it reports is either normal or self-correcting
|
|
3628
|
+
on the next `cctally quota` run, and `doctor` FAIL exits 2.
|
|
3629
|
+
"""
|
|
3630
|
+
state = s.quota_calibration
|
|
3631
|
+
if not state:
|
|
3632
|
+
return CheckResult(
|
|
3633
|
+
id="quota.calibration", title="Calibration",
|
|
3634
|
+
severity="ok", summary="not assessed",
|
|
3635
|
+
remediation=None,
|
|
3636
|
+
details={"rejection": None, "status": None, "present": False},
|
|
3637
|
+
)
|
|
3638
|
+
rejection = state.get("rejection")
|
|
3639
|
+
details = {
|
|
3640
|
+
"rejection": rejection,
|
|
3641
|
+
"status": state.get("status"),
|
|
3642
|
+
"present": bool(state.get("present")),
|
|
3643
|
+
}
|
|
3644
|
+
if rejection is None:
|
|
3645
|
+
return CheckResult(
|
|
3646
|
+
id="quota.calibration", title="Calibration",
|
|
3647
|
+
severity="ok", summary="fitted and usable",
|
|
3648
|
+
remediation=None, details=details,
|
|
3649
|
+
)
|
|
3650
|
+
warn = _QUOTA_CALIBRATION_WARN.get(str(rejection))
|
|
3651
|
+
if warn is not None:
|
|
3652
|
+
return CheckResult(
|
|
3653
|
+
id="quota.calibration", title="Calibration",
|
|
3654
|
+
severity="warn", summary=warn,
|
|
3655
|
+
remediation="Run `cctally quota` to refit it.",
|
|
3656
|
+
details=details,
|
|
3657
|
+
)
|
|
3658
|
+
return CheckResult(
|
|
3659
|
+
id="quota.calibration", title="Calibration",
|
|
3660
|
+
severity="ok",
|
|
3661
|
+
summary=_QUOTA_CALIBRATION_OK.get(
|
|
3662
|
+
str(rejection), "no usable calibration"),
|
|
3663
|
+
remediation=None, details=details,
|
|
3664
|
+
)
|
|
3665
|
+
|
|
3666
|
+
|
|
3667
|
+
def _check_quota_meter_drift(s: DoctorState) -> CheckResult:
|
|
3668
|
+
"""OK-with-detail, stating whether §6.6's marker predicate holds.
|
|
3669
|
+
|
|
3670
|
+
The predicate is DERIVED, never stored: the active open regime has a
|
|
3671
|
+
confirmed predecessor, and the marker shows for the whole of that
|
|
3672
|
+
successor regime. A rate change is the provider's behaviour rather than a
|
|
3673
|
+
cctally malfunction, so this check states it and never warns.
|
|
3674
|
+
"""
|
|
3675
|
+
state = s.quota_rate_change
|
|
3676
|
+
if not state:
|
|
3677
|
+
return CheckResult(
|
|
3678
|
+
id="quota.meter_drift", title="Metering rate",
|
|
3679
|
+
severity="ok", summary="not assessed",
|
|
3680
|
+
remediation=None, details={"active": False},
|
|
3681
|
+
)
|
|
3682
|
+
details = {
|
|
3683
|
+
"active": bool(state.get("active")),
|
|
3684
|
+
"effective_from": state.get("effective_from"),
|
|
3685
|
+
"previous_units_per_point": state.get("previous_units_per_point"),
|
|
3686
|
+
"new_units_per_point": state.get("new_units_per_point"),
|
|
3687
|
+
}
|
|
3688
|
+
if not details["active"]:
|
|
3689
|
+
return CheckResult(
|
|
3690
|
+
id="quota.meter_drift", title="Metering rate",
|
|
3691
|
+
severity="ok", summary="no change detected",
|
|
3692
|
+
remediation=None, details=details,
|
|
3693
|
+
)
|
|
3694
|
+
# The stamp is a clock INSTANT, so a bare `YYYY-MM-DD` slice of its text
|
|
3695
|
+
# names the UTC calendar day while reading as the user's own (#661 S2
|
|
3696
|
+
# Stage C review, F8). This kernel has no display-timezone plumbing —
|
|
3697
|
+
# every other datetime it emits is UTC through `_iso_z` — so the day is
|
|
3698
|
+
# parsed rather than sliced, normalized to UTC, and LABELLED as UTC.
|
|
3699
|
+
effective = _utc_day_label(details["effective_from"])
|
|
3700
|
+
summary = (f"rate changed {effective} UTC" if effective
|
|
3701
|
+
else "rate changed")
|
|
3702
|
+
return CheckResult(
|
|
3703
|
+
id="quota.meter_drift", title="Metering rate",
|
|
3704
|
+
severity="ok", summary=summary,
|
|
3705
|
+
remediation="Run `cctally quota` for the fitted budget.",
|
|
3706
|
+
details=details,
|
|
3707
|
+
)
|
|
3708
|
+
|
|
3709
|
+
|
|
3552
3710
|
def _evaluate_one(check_id: str, check_fn_name: str,
|
|
3553
3711
|
state: DoctorState) -> CheckResult:
|
|
3554
3712
|
"""Invoke a single check evaluator by name, catching any exception so
|