cctally 1.102.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 +1274 -1389
- package/README.md +3 -3
- package/bin/_cctally_alerts.py +65 -4
- package/bin/_cctally_cache.py +31 -0
- 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_jsonl.py +102 -5
- package/bin/_lib_meter_rate_change.py +294 -0
- package/bin/_lib_pricing.py +97 -21
- 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_journal.py
CHANGED
|
@@ -3611,7 +3611,16 @@ _ACCOUNTS_MACHINERY_KINDS = frozenset(
|
|
|
3611
3611
|
# STRUCTURALLY by tests/test_accounts_journal.py (iterating both spec registries),
|
|
3612
3612
|
# so a future data-bearing kind cannot silently escape classification.
|
|
3613
3613
|
_CLASSIFIER_VENDOR_TAGGED_KINDS = frozenset(("budget",))
|
|
3614
|
-
|
|
3614
|
+
# `meter_rate_change` (#661 S2 §6.4) is EXEMPT for the same structural reason
|
|
3615
|
+
# `weekly_credit_effects` is: this classifier exists to normalise the account
|
|
3616
|
+
# of a LEGACY, unstamped line, and no such line can exist for this family. It
|
|
3617
|
+
# was introduced with epoch 1011, every emission carries `payload.account_key`
|
|
3618
|
+
# from the first one, and its target table has no unstamped history to
|
|
3619
|
+
# normalise. Filing it under `_EVT_KIND_PROVIDER` would claim a fixed vendor
|
|
3620
|
+
# for a family whose provider is a payload field, which is the failure the
|
|
3621
|
+
# vendor-tagged case exists to avoid.
|
|
3622
|
+
_CLASSIFIER_EXEMPT_KINDS = frozenset((
|
|
3623
|
+
"weekly_credit_effects", "meter_rate_change"))
|
|
3615
3624
|
|
|
3616
3625
|
|
|
3617
3626
|
def classify_legacy_provider(record) -> str | None:
|
|
@@ -3893,6 +3902,70 @@ def _apply_quota_alert_arming(conn, evt):
|
|
|
3893
3902
|
return None
|
|
3894
3903
|
|
|
3895
3904
|
|
|
3905
|
+
def _meter_rate_change_row(evt) -> "tuple | None":
|
|
3906
|
+
"""The row `meter_rate_change_events` takes, or None for a bad payload.
|
|
3907
|
+
|
|
3908
|
+
Normalising rather than trusting the payload keeps a malformed record from
|
|
3909
|
+
raising here and prefix-stopping the whole fold.
|
|
3910
|
+
"""
|
|
3911
|
+
p = evt.get("payload") or {}
|
|
3912
|
+
provider = p.get("provider")
|
|
3913
|
+
effective_from = p.get("effective_from")
|
|
3914
|
+
if not provider or not effective_from:
|
|
3915
|
+
return None
|
|
3916
|
+
try:
|
|
3917
|
+
previous = float(p.get("previous_units_per_point"))
|
|
3918
|
+
new = float(p.get("new_units_per_point"))
|
|
3919
|
+
except (TypeError, ValueError):
|
|
3920
|
+
return None
|
|
3921
|
+
account_key = p.get("account_key") or _lib_accounts.UNATTRIBUTED
|
|
3922
|
+
created = p.get("created_at_utc") or evt.get("at")
|
|
3923
|
+
return (str(provider), str(account_key), str(effective_from), previous,
|
|
3924
|
+
new, str(p.get("severity") or "info"),
|
|
3925
|
+
str(p.get("detected_at_utc") or created or ""), str(created or ""))
|
|
3926
|
+
|
|
3927
|
+
|
|
3928
|
+
def _insert_meter_rate_change(conn, evt) -> bool:
|
|
3929
|
+
"""Insert one rate-change row. True when THIS call created it.
|
|
3930
|
+
|
|
3931
|
+
The return value is the §6.5 step-5 predicate: a notification is queued
|
|
3932
|
+
only when the insert actually created a row, so a crash-replayed
|
|
3933
|
+
duplicate, an ordinary re-run and a rebuild all converge silently. It is
|
|
3934
|
+
`rowcount`, never `lastrowid` — the same predicate the milestone families
|
|
3935
|
+
use, because `lastrowid` is left over from a previous insert when
|
|
3936
|
+
`INSERT OR IGNORE` ignores.
|
|
3937
|
+
"""
|
|
3938
|
+
row = _meter_rate_change_row(evt)
|
|
3939
|
+
if row is None:
|
|
3940
|
+
return False
|
|
3941
|
+
cur = conn.execute(
|
|
3942
|
+
"INSERT OR IGNORE INTO meter_rate_change_events "
|
|
3943
|
+
"(provider, account_key, effective_from, previous_units_per_point,"
|
|
3944
|
+
" new_units_per_point, severity, detected_at_utc, created_at_utc) "
|
|
3945
|
+
"VALUES (?,?,?,?,?,?,?,?)", row)
|
|
3946
|
+
return cur.rowcount == 1
|
|
3947
|
+
|
|
3948
|
+
|
|
3949
|
+
def _apply_meter_rate_change(conn, evt):
|
|
3950
|
+
"""Fold a `meter_rate_change` evt (#661 S2 §6.4/§6.5).
|
|
3951
|
+
|
|
3952
|
+
`meter_rate_change_events` is the durable forward-only latch that one
|
|
3953
|
+
provider metering-rate transition was recorded. It has no `journal_id`
|
|
3954
|
+
column, so idempotence rides the natural-key `INSERT OR IGNORE` exactly as
|
|
3955
|
+
`quota_alert_arming` and `quota_threshold_events` do rather than an
|
|
3956
|
+
`INSERT OR IGNORE` on the journal id.
|
|
3957
|
+
|
|
3958
|
+
Replay reaches this applier with NO `IngestContext`, so it is
|
|
3959
|
+
structurally unable to queue a notification — invariant (iv), and the
|
|
3960
|
+
reason a rebuild cannot re-fire history. `notified_at` is deliberately not
|
|
3961
|
+
journaled and not touched here: whether a notification was dispatched is a
|
|
3962
|
+
local delivery fact, not a durable one, and replaying a stale value would
|
|
3963
|
+
fight the live path.
|
|
3964
|
+
"""
|
|
3965
|
+
_insert_meter_rate_change(conn, evt)
|
|
3966
|
+
return None
|
|
3967
|
+
|
|
3968
|
+
|
|
3896
3969
|
def _apply_quota_threshold_event(conn, evt):
|
|
3897
3970
|
"""Fold a `quota_threshold_event` evt (#416 spec §7.2, review F13).
|
|
3898
3971
|
|
|
@@ -4158,6 +4231,12 @@ _EVT_SPECS = {
|
|
|
4158
4231
|
# insert is `INSERT OR IGNORE`, so neither can clobber the other's row.
|
|
4159
4232
|
"quota_threshold_event": _EvtSpec(
|
|
4160
4233
|
None, order=44, applier=_apply_quota_threshold_event),
|
|
4234
|
+
# #661 S2 §6.4. An independent stats.db table with no FK into the
|
|
4235
|
+
# journal-covered families and its own natural-key insert applier, so the
|
|
4236
|
+
# order is arbitrary among evts. 43 keeps it beside the other two quota
|
|
4237
|
+
# state families rather than implying a dependency it does not have.
|
|
4238
|
+
"meter_rate_change": _EvtSpec(
|
|
4239
|
+
None, order=43, applier=_apply_meter_rate_change),
|
|
4161
4240
|
}
|
|
4162
4241
|
for _hs in _HARVEST_SPECS:
|
|
4163
4242
|
if _hs.children:
|
|
@@ -4388,6 +4467,54 @@ def _pipeline_op_fold(ctx, record) -> None:
|
|
|
4388
4467
|
PIPELINE.append(_pipeline_op_fold)
|
|
4389
4468
|
|
|
4390
4469
|
|
|
4470
|
+
def record_meter_rate_change(ctx, transition, *, notify: bool,
|
|
4471
|
+
created_at: str) -> bool:
|
|
4472
|
+
"""Steps 4 and 5 of spec §6.5, run INSIDE the cycle transaction.
|
|
4473
|
+
|
|
4474
|
+
Journal-first (invariant i): the evt line is appended and fsync'd through
|
|
4475
|
+
the leaf `journal.lock` before the transaction containing its row commits,
|
|
4476
|
+
exactly as `emit_model_a` does. The payload is a pure function of the
|
|
4477
|
+
transition, so a crash-replayed duplicate line is byte-identical and folds
|
|
4478
|
+
to a clean no-op.
|
|
4479
|
+
|
|
4480
|
+
Returns whether THIS call created the row. A notification is queued only
|
|
4481
|
+
then, and only when `notify` is true — §6.2 splits recording from
|
|
4482
|
+
notifying, so the row exists from the first upgrade while the push follows
|
|
4483
|
+
the existing default-off toggle. The queue is `ctx.pending_alerts`, which
|
|
4484
|
+
step 6 drains AFTER the commit: committed-before-notify, and a crash
|
|
4485
|
+
between the two loses at most one dispatch rather than recording an alert
|
|
4486
|
+
that never happened.
|
|
4487
|
+
"""
|
|
4488
|
+
mrc = _load_meter_rate_change()
|
|
4489
|
+
payload = mrc.event_payload(transition, created_at=created_at)
|
|
4490
|
+
eid = _lib_journal.evt_id(
|
|
4491
|
+
mrc.EVT_ID_PREFIX, transition.provider, transition.account_key,
|
|
4492
|
+
transition.effective_from)
|
|
4493
|
+
evt = _lib_journal.make_evt(
|
|
4494
|
+
kind=mrc.EVT_KIND, id=eid, at=created_at, payload=payload)
|
|
4495
|
+
append_record(evt)
|
|
4496
|
+
ctx.events_emitted += 1
|
|
4497
|
+
created = _insert_meter_rate_change(ctx.conn, evt)
|
|
4498
|
+
if created and notify:
|
|
4499
|
+
ctx.pending_alerts.append(mrc.alert_payload(transition))
|
|
4500
|
+
return created
|
|
4501
|
+
|
|
4502
|
+
|
|
4503
|
+
def _load_meter_rate_change():
|
|
4504
|
+
"""The pure kernel, via the call-time sibling accessor.
|
|
4505
|
+
|
|
4506
|
+
A module-level `import _lib_meter_rate_change` would be honest here, but
|
|
4507
|
+
this module is imported by the kernel-extraction paths that must not grow
|
|
4508
|
+
new import-time edges; every other optional sibling in this file is
|
|
4509
|
+
reached the same way.
|
|
4510
|
+
"""
|
|
4511
|
+
cctally = sys.modules.get("cctally")
|
|
4512
|
+
if cctally is not None and hasattr(cctally, "_load_sibling"):
|
|
4513
|
+
return cctally._load_sibling("_lib_meter_rate_change")
|
|
4514
|
+
import _lib_meter_rate_change
|
|
4515
|
+
return _lib_meter_rate_change
|
|
4516
|
+
|
|
4517
|
+
|
|
4391
4518
|
def _dispatch_pending_alerts(alerts: list) -> None:
|
|
4392
4519
|
"""Default post-commit dispatch (spec §5.2 step 6): fire each queued alert
|
|
4393
4520
|
payload through the cctally dispatch glue (bin/_lib_alert_dispatch via
|
|
@@ -6049,7 +6176,8 @@ def _preflight_live_events(
|
|
|
6049
6176
|
# --------------------------------------------------------------------------
|
|
6050
6177
|
|
|
6051
6178
|
def _run_cycle(conn: sqlite3.Connection, *, reconcile_config=None,
|
|
6052
|
-
codex_apply=None, post_commit=None
|
|
6179
|
+
codex_apply=None, post_commit=None,
|
|
6180
|
+
meter_rate_change=None) -> IngestResult:
|
|
6053
6181
|
# Step 1: HW snapshot (leaf lock, µs). Lines appended after this — by other
|
|
6054
6182
|
# processes OR by this cycle's own evt emission — are past HW and belong to
|
|
6055
6183
|
# the next cycle (§5.2.1, closes the skipped-append race).
|
|
@@ -6074,7 +6202,8 @@ def _run_cycle(conn: sqlite3.Connection, *, reconcile_config=None,
|
|
|
6074
6202
|
cursor = None
|
|
6075
6203
|
cursor_target = None
|
|
6076
6204
|
if hw is None:
|
|
6077
|
-
if reconcile_config is None and codex_apply is None
|
|
6205
|
+
if (reconcile_config is None and codex_apply is None
|
|
6206
|
+
and meter_rate_change is None):
|
|
6078
6207
|
return IngestResult(ran=True, consumed=0, malformed=0,
|
|
6079
6208
|
events_emitted=0, alerts=[])
|
|
6080
6209
|
else:
|
|
@@ -6190,6 +6319,18 @@ def _run_cycle(conn: sqlite3.Connection, *, reconcile_config=None,
|
|
|
6190
6319
|
# the whole cycle back (invariant ii).
|
|
6191
6320
|
if codex_apply is not None:
|
|
6192
6321
|
codex_apply(ctx)
|
|
6322
|
+
# 4b'''. #661 S2 §6.5 steps 4-5. A confirmed metering-rate transition,
|
|
6323
|
+
# already decided under the calibration file's leaf lock and released
|
|
6324
|
+
# before any stats lock was taken (step 2), is journaled and applied
|
|
6325
|
+
# here — inside this transaction, journal-first, with its notification
|
|
6326
|
+
# queued to `ctx.pending_alerts` for the post-commit dispatch below.
|
|
6327
|
+
# It sits beside the Codex leg rather than in the pipeline because its
|
|
6328
|
+
# trigger is a persistence transition rather than a journal record.
|
|
6329
|
+
if meter_rate_change is not None:
|
|
6330
|
+
record_meter_rate_change(
|
|
6331
|
+
ctx, meter_rate_change["transition"],
|
|
6332
|
+
notify=bool(meter_rate_change.get("notify")),
|
|
6333
|
+
created_at=str(meter_rate_change["created_at"]))
|
|
6193
6334
|
# 4c. Journal + stamp the natural-keyed rows the pipeline inserted.
|
|
6194
6335
|
# Early-out (Task 6 gate P2): the ONLY source of `journal_id IS NULL`
|
|
6195
6336
|
# rows is a Task-5 chokepoint called from a step-4b pipeline hook —
|
|
@@ -6267,6 +6408,7 @@ def _run_stats_ingest_once(
|
|
|
6267
6408
|
reconcile_config=None,
|
|
6268
6409
|
codex_apply=None,
|
|
6269
6410
|
post_commit=None,
|
|
6411
|
+
meter_rate_change=None,
|
|
6270
6412
|
locks_held: bool = False,
|
|
6271
6413
|
) -> IngestResult:
|
|
6272
6414
|
"""Run one single-flight attempt, without correction-recovery orchestration.
|
|
@@ -6439,7 +6581,8 @@ def _run_stats_ingest_once(
|
|
|
6439
6581
|
with _cctally_store.stats_write_scope("ingest", ingest_lock=True):
|
|
6440
6582
|
return _run_cycle(conn, reconcile_config=reconcile_config,
|
|
6441
6583
|
codex_apply=codex_apply,
|
|
6442
|
-
post_commit=post_commit
|
|
6584
|
+
post_commit=post_commit,
|
|
6585
|
+
meter_rate_change=meter_rate_change)
|
|
6443
6586
|
except CorrectionRebuildRequired:
|
|
6444
6587
|
# The public boundary must unwind its transaction, internally owned
|
|
6445
6588
|
# connection, ingest lock, and maintenance-shared lock before it can
|
|
@@ -6705,6 +6848,7 @@ def run_stats_ingest(
|
|
|
6705
6848
|
reconcile_config=None,
|
|
6706
6849
|
codex_apply=None,
|
|
6707
6850
|
post_commit=None,
|
|
6851
|
+
meter_rate_change=None,
|
|
6708
6852
|
locks_held: bool = False,
|
|
6709
6853
|
) -> IngestResult:
|
|
6710
6854
|
"""Run one cycle, healing one completed-correction mismatch when safe.
|
|
@@ -6730,6 +6874,7 @@ def run_stats_ingest(
|
|
|
6730
6874
|
"reconcile_config": reconcile_config,
|
|
6731
6875
|
"codex_apply": codex_apply,
|
|
6732
6876
|
"post_commit": post_commit,
|
|
6877
|
+
"meter_rate_change": meter_rate_change,
|
|
6733
6878
|
"locks_held": locks_held,
|
|
6734
6879
|
}
|
|
6735
6880
|
try:
|
|
@@ -6841,6 +6986,7 @@ _REBUILD_COUNT_TABLES = (
|
|
|
6841
6986
|
"five_hour_milestones", "budget_milestones", "projected_milestones",
|
|
6842
6987
|
"project_budget_milestones", "quota_alert_arming", "quota_window_blocks",
|
|
6843
6988
|
"quota_percent_milestones", "quota_threshold_events", "accounts",
|
|
6989
|
+
"meter_rate_change_events",
|
|
6844
6990
|
)
|
|
6845
6991
|
|
|
6846
6992
|
|
|
@@ -7014,6 +7160,7 @@ _REBUILD_REQUIRED_TABLES = frozenset(
|
|
|
7014
7160
|
"journal_selector_batch_records",
|
|
7015
7161
|
"journal_selector_batches",
|
|
7016
7162
|
"journal_selector_state",
|
|
7163
|
+
"meter_rate_change_events",
|
|
7017
7164
|
"percent_milestones",
|
|
7018
7165
|
"project_budget_milestones",
|
|
7019
7166
|
"projected_milestones",
|
|
@@ -7053,6 +7200,7 @@ _REBUILD_REQUIRED_INDEXES = frozenset(
|
|
|
7053
7200
|
"idx_five_hour_reset_events_journal_id",
|
|
7054
7201
|
"idx_five_hour_reset_events_journal_id_null",
|
|
7055
7202
|
"idx_journal_protocol_violations_batch",
|
|
7203
|
+
"idx_meter_rate_change_events_key",
|
|
7056
7204
|
"idx_percent_milestones_journal_id",
|
|
7057
7205
|
"idx_percent_milestones_journal_id_null",
|
|
7058
7206
|
"idx_project_budget_milestones_journal_id",
|
|
@@ -7077,7 +7225,7 @@ _REBUILD_REQUIRED_INDEXES = frozenset(
|
|
|
7077
7225
|
# omitted column, constraint, partial predicate, or index definition. An epoch
|
|
7078
7226
|
# schema change must update this contract alongside STATS_INDEX_EPOCH.
|
|
7079
7227
|
_REBUILD_SCHEMA_FINGERPRINT = (
|
|
7080
|
-
"
|
|
7228
|
+
"472e77f23b289eb9141c2b318b94f24e98d509a73a9063568fbd66b04013ead3"
|
|
7081
7229
|
)
|
|
7082
7230
|
|
|
7083
7231
|
|
package/bin/_cctally_parser.py
CHANGED
|
@@ -3488,6 +3488,71 @@ def _build_pricing_check_parser(subparsers, name, *, help_text, xref=None):
|
|
|
3488
3488
|
)
|
|
3489
3489
|
pc_p.set_defaults(func=c.cmd_pricing_check)
|
|
3490
3490
|
|
|
3491
|
+
def _build_quota_parser(subparsers, name, *, help_text, xref=None):
|
|
3492
|
+
"""Build the top-level `quota` parser (#661 S1, spec section 8).
|
|
3493
|
+
|
|
3494
|
+
Registered ONLY at top level. There is no `claude quota` twin and no Bash
|
|
3495
|
+
wrapper: `_build_claude_parser` enumerates its leaves by hand and
|
|
3496
|
+
`_REGISTRATION` enumerates the flat commands separately, so nothing
|
|
3497
|
+
mirrors automatically and there is no accidental duplication to prevent.
|
|
3498
|
+
"""
|
|
3499
|
+
c = _cctally()
|
|
3500
|
+
q = subparsers.add_parser(
|
|
3501
|
+
name,
|
|
3502
|
+
help=help_text,
|
|
3503
|
+
formatter_class=CLIHelpFormatter,
|
|
3504
|
+
description=textwrap.dedent(
|
|
3505
|
+
"""\
|
|
3506
|
+
Report how much of your weekly Claude quota your usage consumes,
|
|
3507
|
+
computed from tokens against a budget fitted from your own
|
|
3508
|
+
history, and whether the provider has changed the metering rate.
|
|
3509
|
+
|
|
3510
|
+
The fitted value is one effective blended rate in weighted units
|
|
3511
|
+
per weekly point. It is valid for the model and token blend your
|
|
3512
|
+
own history shows, it is NOT a provider budget, and it is the
|
|
3513
|
+
provider's budget minus an unmeasured, workload-dependent amount.
|
|
3514
|
+
|
|
3515
|
+
Exit codes:
|
|
3516
|
+
0 — trustworthy result, no rate change detected.
|
|
3517
|
+
1 — a rate change is confirmed. Detection has its own stricter
|
|
3518
|
+
gates than prediction, so this can be reported while the
|
|
3519
|
+
calibration itself is still too thin to predict from.
|
|
3520
|
+
2 — argument or validation error.
|
|
3521
|
+
3 — judgment withheld: the data is unhealthy or a store is
|
|
3522
|
+
unavailable.
|
|
3523
|
+
4 — the evidence is healthy but too thin, fragmented or
|
|
3524
|
+
unstable to fit.
|
|
3525
|
+
|
|
3526
|
+
See docs/commands/quota.md for the JSON schema.
|
|
3527
|
+
"""
|
|
3528
|
+
),
|
|
3529
|
+
)
|
|
3530
|
+
q.add_argument(
|
|
3531
|
+
"--since", metavar="DATE",
|
|
3532
|
+
help="Ignore data before this ISO date or timestamp. It never widens "
|
|
3533
|
+
"the window past the supported-composition era.",
|
|
3534
|
+
)
|
|
3535
|
+
q.add_argument(
|
|
3536
|
+
"--watch-from", metavar="DATE",
|
|
3537
|
+
help="Evaluate this split date instead of scanning for one. Bypasses "
|
|
3538
|
+
"the scan and the multiplicity correction, never the health or "
|
|
3539
|
+
"fit gates. A run passing it persists nothing.",
|
|
3540
|
+
)
|
|
3541
|
+
q.add_argument(
|
|
3542
|
+
"--account", metavar="REF",
|
|
3543
|
+
help="Scope to one account (label, email or unique key prefix)",
|
|
3544
|
+
)
|
|
3545
|
+
q.add_argument(
|
|
3546
|
+
"--json", action="store_true",
|
|
3547
|
+
help="Emit machine-readable JSON to stdout (schemaVersion: 1)",
|
|
3548
|
+
)
|
|
3549
|
+
q.add_argument(
|
|
3550
|
+
"--reset-calibration", action="store_true",
|
|
3551
|
+
help="Discard the stored calibration for the selected account",
|
|
3552
|
+
)
|
|
3553
|
+
q.set_defaults(func=c.cmd_quota)
|
|
3554
|
+
|
|
3555
|
+
|
|
3491
3556
|
def _build_hook_tick_parser(subparsers, name, *, help_text, xref=None):
|
|
3492
3557
|
"""Build the `hook-tick` parser (registered via _REGISTRATION; #279 S6 W3).
|
|
3493
3558
|
|
|
@@ -3830,6 +3895,7 @@ _REGISTRATION = (
|
|
|
3830
3895
|
_Reg('doctor', _build_doctor_parser, "Diagnose data freshness and install state", None, None),
|
|
3831
3896
|
_Reg('dashboard-perf', _build_dashboard_perf_parser, "Read a running dashboard's tick cost; arm its phase trace", None, None),
|
|
3832
3897
|
_Reg('pricing-check', _build_pricing_check_parser, "Detect stale or missing embedded model pricing", None, None),
|
|
3898
|
+
_Reg('quota', _build_quota_parser, "Report weekly quota consumption and metering-rate changes", None, None),
|
|
3833
3899
|
_Reg('hook-tick', _build_hook_tick_parser, argparse.SUPPRESS, None, None),
|
|
3834
3900
|
_Reg('__preview', _build_preview_parser, argparse.SUPPRESS, None, lambda c: getattr(c, "cmd_preview", None) is not None),
|
|
3835
3901
|
_Reg('update', _build_update_parser, "Update cctally to the latest version", None, None),
|