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_record.py
CHANGED
|
@@ -1493,9 +1493,69 @@ def maybe_record_codex_budget_milestone(
|
|
|
1493
1493
|
return fired
|
|
1494
1494
|
|
|
1495
1495
|
|
|
1496
|
-
def
|
|
1496
|
+
def _forecast_reaches_the_calibrated_basis(
|
|
1497
|
+
now_utc, week_start_at, week_end_at, *, account_key=None) -> bool:
|
|
1498
|
+
"""True when the FORECAST would publish a model-backed projection here.
|
|
1499
|
+
|
|
1500
|
+
Spec section 3.3 conditions the twin's abstention on the alert path being
|
|
1501
|
+
unable to reach "the basis the forecast published" — not on a regime
|
|
1502
|
+
merely validating. Those are different questions, and the gap between
|
|
1503
|
+
them is reachable: `apply_regime` re-tests support against THIS week's
|
|
1504
|
+
population and rejects an unsupported or empty one, so a calibration can
|
|
1505
|
+
validate while the forecast still falls back to the corrected meter. At
|
|
1506
|
+
the start of every week the population is empty, and gating on
|
|
1507
|
+
readability there returned `None` from the twin and silently disabled the
|
|
1508
|
+
weekly 90%/100% projected alert.
|
|
1509
|
+
|
|
1510
|
+
This calls the SAME helper the loader calls, with the SAME account key,
|
|
1511
|
+
so the two cannot answer differently. The earlier probe always read the
|
|
1512
|
+
merged bucket while the loader used its caller's key, which on a
|
|
1513
|
+
decorated multi-account install let the forecast publish a calibrated
|
|
1514
|
+
projection while the twin saw no merged regime and fired on the meter.
|
|
1515
|
+
|
|
1516
|
+
Any failure to reach the helper is reported as "not calibrated", which
|
|
1517
|
+
keeps the twin firing rather than going quiet on an unrelated defect.
|
|
1518
|
+
|
|
1519
|
+
COST. This is NOT a cheap probe once a regime validates. `_calibrated_projection`
|
|
1520
|
+
reads the calibration file first — one small file read on every install
|
|
1521
|
+
that has never run `cctally quota`, and the common case — but when a
|
|
1522
|
+
regime does validate it then opens `cache.db` and runs a full current-week
|
|
1523
|
+
`session_entries` SELECT, on a path `record-usage` and `hook-tick` reach
|
|
1524
|
+
once per prompt. Measured over the twelve most recent completed
|
|
1525
|
+
subscription weeks of the maintainer's August 2026 store snapshot
|
|
1526
|
+
(476,216 `session_entries` rows), on an Apple M4 Max Mac Studio under
|
|
1527
|
+
CPython 3.14 with the file cache warm, a WHOLE week is a median 16,923
|
|
1528
|
+
rows and 28 ms and at worst 27,664 rows and 46 ms. This read covers the
|
|
1529
|
+
current week only as far as now, so it is a fraction of that early in
|
|
1530
|
+
the week and reaches it by the week's end. The durations are this
|
|
1531
|
+
machine's and another host will differ; what does not vary is that the
|
|
1532
|
+
read is one unbounded week-scan rather than a bounded probe. A cheap bound is available in principle — the same
|
|
1533
|
+
query with `COUNT(*)`, or a bounded `LIMIT 1` existence probe, would
|
|
1534
|
+
answer the empty-population half of `apply_regime`'s rejection without
|
|
1535
|
+
materializing the rows — but it would not answer the composition-support
|
|
1536
|
+
half, which needs the whole population. Not implemented here.
|
|
1537
|
+
"""
|
|
1538
|
+
try:
|
|
1539
|
+
value, _code = _cctally()._load_sibling(
|
|
1540
|
+
"_cctally_forecast")._calibrated_projection(
|
|
1541
|
+
now_utc, week_start_at, week_end_at, account_key=account_key)
|
|
1542
|
+
return value is not None
|
|
1543
|
+
except Exception:
|
|
1544
|
+
return False
|
|
1545
|
+
|
|
1546
|
+
|
|
1547
|
+
def _weekly_pct_week_avg_projection(conn, now_utc, *, account_key=None):
|
|
1497
1548
|
"""Compute the week-AVERAGE weekly-% projection for the current
|
|
1498
|
-
subscription week
|
|
1549
|
+
subscription week from snapshots alone.
|
|
1550
|
+
|
|
1551
|
+
``account_key`` is an AFFORDANCE, not a threaded production value. The one
|
|
1552
|
+
shipped caller (the projected-alert leg in ``maybe_record_projected_alert``)
|
|
1553
|
+
passes none, and the window it resolves just above comes from a merged
|
|
1554
|
+
``_fetch_current_week_snapshots``, so the whole projected-alert path is
|
|
1555
|
+
account-blind today. Making it per-account is a #341 change to that leg
|
|
1556
|
+
rather than to this helper, and this session does not make it. The
|
|
1557
|
+
parameter exists so the abstention gate below can be given the SAME key
|
|
1558
|
+
the forecast loader would use once the leg is threaded.
|
|
1499
1559
|
|
|
1500
1560
|
Returns ``(projected_pct, low_conf)`` or ``None`` when no current-week
|
|
1501
1561
|
snapshot resolves. The value is computed by the IDENTICAL formula +
|
|
@@ -1518,12 +1578,33 @@ def _weekly_pct_week_avg_projection(conn, now_utc):
|
|
|
1518
1578
|
|
|
1519
1579
|
Deliberately does NOT call ``_sum_cost_for_range`` (the weekly-% projection
|
|
1520
1580
|
needs no spend; the forecast kernel's ``week_avg_projection_pct`` is also
|
|
1521
|
-
spend-free)
|
|
1581
|
+
spend-free), and never calls ``sync_cache``.
|
|
1582
|
+
|
|
1583
|
+
It is NOT, however, snapshot-only any more. The #661 S2 abstention gate
|
|
1584
|
+
below calls ``_forecast_reaches_the_calibrated_basis``, which reads the
|
|
1585
|
+
calibration file and — when a regime validates — opens ``cache.db`` for a
|
|
1586
|
+
full current-week ``session_entries`` SELECT. See that helper's COST note.
|
|
1522
1587
|
"""
|
|
1523
|
-
fetched = _fetch_current_week_snapshots(conn, now_utc
|
|
1588
|
+
fetched = _fetch_current_week_snapshots(conn, now_utc,
|
|
1589
|
+
account_key=account_key)
|
|
1524
1590
|
if fetched is None:
|
|
1525
1591
|
return None
|
|
1526
1592
|
week_start_at, week_end_at, samples = fetched
|
|
1593
|
+
# #661 S2 spec section 3.3, invariant PROJECTED1-b. This helper computes
|
|
1594
|
+
# the week average from snapshots alone, and the calibrated basis needs
|
|
1595
|
+
# the current week's weighted units. So when the FORECAST reaches that
|
|
1596
|
+
# basis it publishes a model-backed projection this value cannot equal,
|
|
1597
|
+
# and the twin ABSTAINS rather than firing on the corrected meter, which
|
|
1598
|
+
# would alert on a number the screen does not show.
|
|
1599
|
+
#
|
|
1600
|
+
# The gate is the basis the forecast actually reached, resolved with the
|
|
1601
|
+
# window this helper just resolved, so a calibration that validates but
|
|
1602
|
+
# does not apply to this week's population leaves the twin firing. It
|
|
1603
|
+
# never calls `analyse_account`: the probe reads the calibration file and,
|
|
1604
|
+
# only when a regime validates, one bounded current-week query.
|
|
1605
|
+
if _forecast_reaches_the_calibrated_basis(
|
|
1606
|
+
now_utc, week_start_at, week_end_at, account_key=account_key):
|
|
1607
|
+
return None
|
|
1527
1608
|
week_start_at, samples = _apply_midweek_reset_override(
|
|
1528
1609
|
conn, week_start_at, week_end_at, samples
|
|
1529
1610
|
)
|
|
@@ -1532,8 +1613,35 @@ def _weekly_pct_week_avg_projection(conn, now_utc):
|
|
|
1532
1613
|
p_now = samples[-1][1]
|
|
1533
1614
|
elapsed_hours = (now_utc - week_start_at).total_seconds() / 3600.0
|
|
1534
1615
|
remaining_hours = max(0.0, (week_end_at - now_utc).total_seconds() / 3600.0)
|
|
1535
|
-
|
|
1536
|
-
|
|
1616
|
+
# #661 S2 spec section 3.1/3.3: the same CEILING-CORRECTED operand
|
|
1617
|
+
# `_compute_forecast` projects from. The reconcile invariant PROJECTED1
|
|
1618
|
+
# binds this value to `forecast --json`'s `week_avg_projection_pct`
|
|
1619
|
+
# within 1e-9, so the two must share the operand as well as the formula.
|
|
1620
|
+
# A right-censored reading has no corrected point and therefore no
|
|
1621
|
+
# projection at all: the detector abstains rather than firing on a
|
|
1622
|
+
# number the observation cannot supply.
|
|
1623
|
+
# Imported HERE rather than at module scope, and NOT for a measured
|
|
1624
|
+
# saving. An earlier revision of this comment claimed the module-scope
|
|
1625
|
+
# form cost `record-usage` and `hook-tick` 18.5 ms per run, about 11 ms of
|
|
1626
|
+
# it `_lib_quota_model` "which neither command previously loaded at all".
|
|
1627
|
+
# That is false: `bin/cctally` loads `_lib_quota_model` at line 574 and
|
|
1628
|
+
# `_cctally_forecast` at line 1667, both unconditionally, and
|
|
1629
|
+
# `_cctally_forecast` honest-imports `_lib_forecast` at its own module
|
|
1630
|
+
# top. By the time either command reaches this helper the module is
|
|
1631
|
+
# already in `sys.modules`, and the incremental import measures 0.000 ms.
|
|
1632
|
+
# 18.5 ms is the COLD import into a bare interpreter, which no shipped
|
|
1633
|
+
# path performs. What the local form does buy is correctness: it pairs
|
|
1634
|
+
# with `_ensure_sibling_loaded`, which the module-scope form omitted, so
|
|
1635
|
+
# it does not depend on `_load_sibling` having happened to insert `bin/`
|
|
1636
|
+
# into `sys.path` at `bin/cctally:156`.
|
|
1637
|
+
_ensure_sibling_loaded("_lib_forecast")
|
|
1638
|
+
from _lib_forecast import corrected_percent_point
|
|
1639
|
+
|
|
1640
|
+
p_corrected = corrected_percent_point(p_now)
|
|
1641
|
+
if p_corrected is None:
|
|
1642
|
+
return None
|
|
1643
|
+
r_avg = p_corrected / elapsed_hours if elapsed_hours > 0 else 0.0
|
|
1644
|
+
projected_pct = p_corrected + r_avg * remaining_hours
|
|
1537
1645
|
|
|
1538
1646
|
# Confidence comes from the predicate in one call, fourth trigger
|
|
1539
1647
|
# included, so this LOW CONF gate == forecast's and glue no longer
|
package/bin/_cctally_share.py
CHANGED
|
@@ -946,8 +946,8 @@ def _build_forecast_snapshot(
|
|
|
946
946
|
actual_series: list[tuple[str, float, float]],
|
|
947
947
|
projected_series: list[tuple[str, float, float]],
|
|
948
948
|
current_pct: float,
|
|
949
|
-
projected_low_pct: float,
|
|
950
|
-
projected_high_pct: float,
|
|
949
|
+
projected_low_pct: "float | None",
|
|
950
|
+
projected_high_pct: "float | None",
|
|
951
951
|
days_remaining: float,
|
|
952
952
|
dollars_per_percent: "float | None",
|
|
953
953
|
dollars_per_percent_source: str,
|
|
@@ -975,10 +975,11 @@ def _build_forecast_snapshot(
|
|
|
975
975
|
wraps `ForecastInputs`). No helper extraction was needed; we pass
|
|
976
976
|
the actual scalars in directly.
|
|
977
977
|
- `ForecastInputs` carries a single `dollars_per_percent` value plus
|
|
978
|
-
a `dollars_per_percent_source`
|
|
979
|
-
`
|
|
980
|
-
|
|
981
|
-
|
|
978
|
+
a `dollars_per_percent_source` code — the six members of
|
|
979
|
+
`_lib_forecast.DOLLARS_PER_PERCENT_SOURCES`, which is the one place
|
|
980
|
+
that union is written down. There is no separate `dpp_week_avg` and
|
|
981
|
+
`dpp_24h`. The table renders one $/1% row with the source's human
|
|
982
|
+
copy as a paren suffix in the metric cell.
|
|
982
983
|
- The plan's single `projected_eow_pct` is split into a low/high
|
|
983
984
|
range (matching `--render-forecast-terminal`'s "Forecast 80–95%"
|
|
984
985
|
band). The table shows both ends; the projected_series ray uses
|
|
@@ -1031,13 +1032,20 @@ def _build_forecast_snapshot(
|
|
|
1031
1032
|
# single value (no recent-24h sample), low == high.
|
|
1032
1033
|
# 0.05 threshold: below .1f display precision — tighter spreads would
|
|
1033
1034
|
# render as identical decimals, so collapse to a single value.
|
|
1034
|
-
|
|
1035
|
+
# #661 S2 spec section 3.2: a right-censored reading has no point
|
|
1036
|
+
# estimate, so the row states the withholding and its cause rather than
|
|
1037
|
+
# printing the current reading under a "Projected" label.
|
|
1038
|
+
if projected_high_pct is None or projected_low_pct is None:
|
|
1039
|
+
projected_text = "withheld — meter at cap"
|
|
1040
|
+
elif abs(projected_high_pct - projected_low_pct) < 0.05:
|
|
1035
1041
|
projected_text = f"{projected_high_pct:.1f}%"
|
|
1036
1042
|
else:
|
|
1037
1043
|
projected_text = (
|
|
1038
1044
|
f"{projected_low_pct:.1f}% — {projected_high_pct:.1f}%"
|
|
1039
1045
|
)
|
|
1040
|
-
dpp_source_label =
|
|
1046
|
+
dpp_source_label = sys.modules["cctally"]._load_sibling(
|
|
1047
|
+
"_lib_forecast").dollars_per_percent_source_label(
|
|
1048
|
+
dollars_per_percent_source)
|
|
1041
1049
|
# #620 S1 D5: a withheld rate renders as the same `n/a` the report trend
|
|
1042
1050
|
# table already uses for an absent $/1%. Forcing it through MoneyCell
|
|
1043
1051
|
# would print $0.00, which is the fabrication this contract removes.
|
|
@@ -1857,6 +1857,39 @@ def _build_statusline_injections(warn_once):
|
|
|
1857
1857
|
return None
|
|
1858
1858
|
return ctx_tokens / window * 100.0
|
|
1859
1859
|
|
|
1860
|
+
def _quota_regimes() -> tuple:
|
|
1861
|
+
"""#661 S2 §9. The stored metering regimes, read without mutating.
|
|
1862
|
+
|
|
1863
|
+
Three properties this read must have, and each is a decision:
|
|
1864
|
+
|
|
1865
|
+
* It takes NO flock and NO throttle. `save_calibrations` writes
|
|
1866
|
+
through `os.replace` in the same directory, so a lock-free reader
|
|
1867
|
+
always sees a complete old or new inode; a flock here would let a
|
|
1868
|
+
writer stall a prompt.
|
|
1869
|
+
* It goes through `read_stored_state_readonly`, not through
|
|
1870
|
+
`load_calibrations`. The loader renames a malformed or
|
|
1871
|
+
version-ahead file aside through `_quarantine`, and a status line
|
|
1872
|
+
that quarantines the user's calibration once per prompt would be a
|
|
1873
|
+
writer on the hottest path in the product.
|
|
1874
|
+
* It catches `OSError` on the open and the read rather than
|
|
1875
|
+
pre-checking existence, which `read_stored_state_readonly` already
|
|
1876
|
+
does — a quarantine rename can remove the primary name between a
|
|
1877
|
+
check and an open, and the reader cannot tell "quarantined" from
|
|
1878
|
+
"absent" without scanning sidecars. It does not scan them; both
|
|
1879
|
+
render the same thing, which is no marker.
|
|
1880
|
+
|
|
1881
|
+
It also NEVER opens `cache.db` and never calls `analyse`. The
|
|
1882
|
+
marker is derived from the regime pair alone.
|
|
1883
|
+
"""
|
|
1884
|
+
try:
|
|
1885
|
+
glue = _cctally()._load_sibling("_cctally_quota_model")
|
|
1886
|
+
state = glue.read_stored_state_readonly()
|
|
1887
|
+
if state is None:
|
|
1888
|
+
return ()
|
|
1889
|
+
return tuple(glue.stored_regimes(state, None))
|
|
1890
|
+
except Exception: # noqa: BLE001
|
|
1891
|
+
return ()
|
|
1892
|
+
|
|
1860
1893
|
return _lib_statusline.StatuslineInjections(
|
|
1861
1894
|
cctally_session_cost=_cctally_session_cost,
|
|
1862
1895
|
today_cost=_today_cost,
|
|
@@ -1865,4 +1898,5 @@ def _build_statusline_injections(warn_once):
|
|
|
1865
1898
|
db_latest_rate_limits=_db_latest_rate_limits,
|
|
1866
1899
|
context_pct=_context_pct,
|
|
1867
1900
|
warn_once=warn_once,
|
|
1901
|
+
quota_regimes=_quota_regimes,
|
|
1868
1902
|
)
|