cctally 1.22.4 → 1.23.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 +11 -0
- package/bin/_cctally_alerts.py +27 -0
- package/bin/_cctally_config.py +68 -9
- package/bin/_cctally_core.py +60 -2
- package/bin/_cctally_dashboard.py +225 -60
- package/bin/_cctally_forecast.py +25 -3
- package/bin/_cctally_milestones.py +68 -0
- package/bin/_cctally_parser.py +10 -2
- package/bin/_cctally_record.py +470 -137
- package/bin/_cctally_tui.py +1 -0
- package/bin/_lib_alert_axes.py +44 -0
- package/bin/_lib_alerts_payload.py +67 -0
- package/bin/_lib_budget.py +8 -0
- package/bin/cctally +12 -0
- package/dashboard/static/assets/index-CXZDQrV3.js +18 -0
- package/dashboard/static/assets/{index-BxmaYT1y.css → index-ZHOC14y-.css} +1 -1
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +2 -1
- package/dashboard/static/assets/index-CLcd-Tnm.js +0 -18
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Pure kernel: alert-axis descriptors + registry + shared severity policy.
|
|
2
|
+
|
|
3
|
+
Single source of truth for axis *metadata* — id, chip/title labels (kept
|
|
4
|
+
byte-identical with dashboard/web/src/lib/alertAxis.ts), the milestone-table
|
|
5
|
+
name used by the dashboard envelope, and the axis-uniform severity policy
|
|
6
|
+
(amber <95 / red >=95). This kernel does NOT own the write/transaction path:
|
|
7
|
+
each axis keeps its own detect-and-arm code in bin/_cctally_record.py. The
|
|
8
|
+
descriptor is the metadata/render contract, not the write engine.
|
|
9
|
+
|
|
10
|
+
Stdlib-only, no I/O at import time. bin/cctally re-exports the public symbols.
|
|
11
|
+
Spec: docs/superpowers/specs/2026-06-01-alerts-axis-registry-projected-pace-design.md
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
|
|
17
|
+
# Severity boundary: thresholds at or above this render red, below it amber.
|
|
18
|
+
# Mirrors the legacy hardcoded amber<95 / red>=95 split (axis-uniform v1).
|
|
19
|
+
_SEVERITY_RED_FLOOR = 95
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def severity_for(threshold: int) -> str:
|
|
23
|
+
"""Map a crossed integer threshold to a severity color ('amber' | 'red')."""
|
|
24
|
+
return "red" if int(threshold) >= _SEVERITY_RED_FLOOR else "amber"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class AlertAxisDescriptor:
|
|
29
|
+
"""Axis-agnostic metadata shared by the record path + dashboard envelope."""
|
|
30
|
+
|
|
31
|
+
id: str # 'weekly' | 'five_hour' | 'budget' | 'projected'
|
|
32
|
+
chip_label: str # SHOUT form, byte-identical with alertAxis.ts AXIS_CHIP_LABEL
|
|
33
|
+
title_label: str # sentence-case form, byte-identical with AXIS_TITLE_LABEL
|
|
34
|
+
milestone_table: str # SQLite table the dashboard envelope SELECTs from
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
AXIS_REGISTRY: "tuple[AlertAxisDescriptor, ...]" = (
|
|
38
|
+
AlertAxisDescriptor("weekly", "WEEKLY", "Weekly", "percent_milestones"),
|
|
39
|
+
AlertAxisDescriptor("five_hour", "5H-BLOCK", "5h-block", "five_hour_milestones"),
|
|
40
|
+
AlertAxisDescriptor("budget", "BUDGET", "Budget", "budget_milestones"),
|
|
41
|
+
AlertAxisDescriptor("projected", "PROJECTED", "Projected", "projected_milestones"),
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
AXIS_BY_ID: "dict[str, AlertAxisDescriptor]" = {d.id: d for d in AXIS_REGISTRY}
|
|
@@ -242,3 +242,70 @@ def _build_alert_payload_budget(
|
|
|
242
242
|
"consumption_pct": float(consumption_pct),
|
|
243
243
|
},
|
|
244
244
|
}
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _alert_text_projected(payload: dict, tz: "ZoneInfo | None") -> tuple[str, str, str]:
|
|
248
|
+
"""Build (title, subtitle, body) for a projected-pace alert (#121).
|
|
249
|
+
|
|
250
|
+
These fire on the WEEK-AVERAGE projection — what you're tracking toward at
|
|
251
|
+
the current week-average pace — NOT on an actual crossing. The text carries
|
|
252
|
+
an explicit "(projection)" / "on current pace" cue so a user never confuses
|
|
253
|
+
a projected alert with an actual-crossing one (which the weekly/budget
|
|
254
|
+
builders render). The rendered numbers come from the payload (the values
|
|
255
|
+
snapshotted at crossing), never from live config (Codex P0-4). ``tz`` is
|
|
256
|
+
accepted for signature parity with peer ``_alert_text_*`` builders and
|
|
257
|
+
intentionally unused (no instant is rendered, same as ``_alert_text_budget``).
|
|
258
|
+
"""
|
|
259
|
+
metric = payload["metric"]
|
|
260
|
+
t = int(payload["threshold"])
|
|
261
|
+
proj = float(payload["projected_value"])
|
|
262
|
+
denom = float(payload["denominator"])
|
|
263
|
+
if metric == "weekly_pct":
|
|
264
|
+
title = f"cctally - projected to reach {t}% this week"
|
|
265
|
+
subtitle = "On current pace (projection)"
|
|
266
|
+
body = f"Projected ~{proj:.0f}% of cap by reset (week-average pace)"
|
|
267
|
+
else: # budget_usd
|
|
268
|
+
title = "cctally - projected to exceed budget"
|
|
269
|
+
subtitle = f"On current pace (projection) - {t}% of budget"
|
|
270
|
+
body = (
|
|
271
|
+
f"Projected ${proj:,.2f} of ${denom:,.2f} budget "
|
|
272
|
+
f"(week-average pace)"
|
|
273
|
+
)
|
|
274
|
+
return title, subtitle, body
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _build_alert_payload_projected(
|
|
278
|
+
*,
|
|
279
|
+
metric: str,
|
|
280
|
+
threshold: int,
|
|
281
|
+
projected_value: float,
|
|
282
|
+
denominator: float,
|
|
283
|
+
week_start_at: str,
|
|
284
|
+
) -> dict:
|
|
285
|
+
"""Build the alert payload for a projected-pace threshold crossing (#121).
|
|
286
|
+
|
|
287
|
+
``axis: "projected"`` is the fourth alert axis; ``metric`` discriminates
|
|
288
|
+
``weekly_pct`` (denominator 100.0, "% of cap") from ``budget_usd``
|
|
289
|
+
(denominator = target_usd, "$ of budget"). The frontend renders context
|
|
290
|
+
FROM these row-sourced fields (``metric`` / ``projected_value`` /
|
|
291
|
+
``denominator``), not from live config that may have changed since crossing
|
|
292
|
+
(Codex P0-4). No ``crossed_at``/``alerted_at`` keys here: the projected
|
|
293
|
+
detector stamps ``alerted_at`` on the DB row itself in the same txn before
|
|
294
|
+
dispatch (set-then-dispatch), and the dashboard envelope reads it from the
|
|
295
|
+
row — mirroring ``_build_alert_payload_budget``'s context-only shape minus
|
|
296
|
+
the redundant timestamp echo.
|
|
297
|
+
"""
|
|
298
|
+
return {
|
|
299
|
+
"id": f"projected:{week_start_at}:{metric}:{int(threshold)}",
|
|
300
|
+
"axis": "projected",
|
|
301
|
+
"metric": str(metric),
|
|
302
|
+
"threshold": int(threshold),
|
|
303
|
+
"projected_value": float(projected_value),
|
|
304
|
+
"denominator": float(denominator),
|
|
305
|
+
"context": {
|
|
306
|
+
"week_start_at": week_start_at,
|
|
307
|
+
"metric": str(metric),
|
|
308
|
+
"projected_value": float(projected_value),
|
|
309
|
+
"denominator": float(denominator),
|
|
310
|
+
},
|
|
311
|
+
}
|
package/bin/_lib_budget.py
CHANGED
|
@@ -52,6 +52,7 @@ class BudgetStatus:
|
|
|
52
52
|
elapsed_fraction: float # [0, 1]
|
|
53
53
|
projected_eow_low_usd: float
|
|
54
54
|
projected_eow_high_usd: float
|
|
55
|
+
week_avg_projection_usd: float # spent + rate_avg*remaining (smooth estimator)
|
|
55
56
|
verdict: str # "ok" | "warn" | "over"
|
|
56
57
|
daily_budget_remaining_usd: float # remaining / remaining-days
|
|
57
58
|
daily_pace_usd: float # current burn $/day (week-average)
|
|
@@ -93,6 +94,12 @@ def compute_budget_status(inputs: BudgetInputs) -> BudgetStatus:
|
|
|
93
94
|
spent, remaining_hours, rate_low, rate_high
|
|
94
95
|
)
|
|
95
96
|
|
|
97
|
+
# Smooth week-average end-of-week projection (additive surface field).
|
|
98
|
+
# Distinct from the displayed band (which keys off the high end): this is
|
|
99
|
+
# the conservative week-average value the projected-pace alert axis fires
|
|
100
|
+
# on. spent + rate_avg*remaining (== project_linear collapsed to one rate).
|
|
101
|
+
week_avg_projection_usd = spent + rate_avg * remaining_hours
|
|
102
|
+
|
|
96
103
|
daily_pace_usd = rate_avg * 24.0
|
|
97
104
|
daily_budget_remaining_usd = (
|
|
98
105
|
(remaining_usd / remaining_days) if remaining_days > 0 else remaining_usd
|
|
@@ -125,6 +132,7 @@ def compute_budget_status(inputs: BudgetInputs) -> BudgetStatus:
|
|
|
125
132
|
elapsed_fraction=elapsed_fraction,
|
|
126
133
|
projected_eow_low_usd=projected_low,
|
|
127
134
|
projected_eow_high_usd=projected_high,
|
|
135
|
+
week_avg_projection_usd=week_avg_projection_usd,
|
|
128
136
|
verdict=verdict,
|
|
129
137
|
daily_budget_remaining_usd=daily_budget_remaining_usd,
|
|
130
138
|
daily_pace_usd=daily_pace_usd,
|
package/bin/cctally
CHANGED
|
@@ -366,14 +366,22 @@ _build_codex_weekly_parser = _cctally_parser._build_codex_weekly_parser
|
|
|
366
366
|
_build_codex_session_parser = _cctally_parser._build_codex_session_parser
|
|
367
367
|
|
|
368
368
|
|
|
369
|
+
_lib_alert_axes = _load_sibling("_lib_alert_axes")
|
|
370
|
+
severity_for = _lib_alert_axes.severity_for
|
|
371
|
+
AlertAxisDescriptor = _lib_alert_axes.AlertAxisDescriptor
|
|
372
|
+
AXIS_REGISTRY = _lib_alert_axes.AXIS_REGISTRY
|
|
373
|
+
AXIS_BY_ID = _lib_alert_axes.AXIS_BY_ID
|
|
374
|
+
|
|
369
375
|
_lib_alerts_payload = _load_sibling("_lib_alerts_payload")
|
|
370
376
|
_alert_text_weekly = _lib_alerts_payload._alert_text_weekly
|
|
371
377
|
_alert_text_five_hour = _lib_alerts_payload._alert_text_five_hour
|
|
372
378
|
_alert_text_budget = _lib_alerts_payload._alert_text_budget
|
|
379
|
+
_alert_text_projected = _lib_alerts_payload._alert_text_projected
|
|
373
380
|
_escape_applescript_string = _lib_alerts_payload._escape_applescript_string
|
|
374
381
|
_build_alert_payload_weekly = _lib_alerts_payload._build_alert_payload_weekly
|
|
375
382
|
_build_alert_payload_five_hour = _lib_alerts_payload._build_alert_payload_five_hour
|
|
376
383
|
_build_alert_payload_budget = _lib_alerts_payload._build_alert_payload_budget
|
|
384
|
+
_build_alert_payload_projected = _lib_alerts_payload._build_alert_payload_projected
|
|
377
385
|
|
|
378
386
|
_lib_five_hour = _load_sibling("_lib_five_hour")
|
|
379
387
|
_FIVE_HOUR_JITTER_FLOOR_SECONDS = _lib_five_hour._FIVE_HOUR_JITTER_FLOOR_SECONDS
|
|
@@ -799,6 +807,8 @@ _PERCENT_NORMALIZE_DECIMALS = _cctally_record._PERCENT_NORMALIZE_DECIMALS
|
|
|
799
807
|
_normalize_percent = _cctally_record._normalize_percent
|
|
800
808
|
maybe_record_milestone = _cctally_record.maybe_record_milestone
|
|
801
809
|
maybe_record_budget_milestone = _cctally_record.maybe_record_budget_milestone
|
|
810
|
+
maybe_record_projected_alert = _cctally_record.maybe_record_projected_alert
|
|
811
|
+
_weekly_pct_week_avg_projection = _cctally_record._weekly_pct_week_avg_projection
|
|
802
812
|
_compute_block_totals = _cctally_record._compute_block_totals
|
|
803
813
|
maybe_update_five_hour_block = _cctally_record.maybe_update_five_hour_block
|
|
804
814
|
cmd_record_usage = _cctally_record.cmd_record_usage
|
|
@@ -2053,6 +2063,8 @@ get_milestone_cost_for_week = _cctally_milestones.get_milestone_cost_for
|
|
|
2053
2063
|
get_milestones_for_week = _cctally_milestones.get_milestones_for_week # forecast c.; tui shim; percent-breakdown c.
|
|
2054
2064
|
insert_percent_milestone = _cctally_milestones.insert_percent_milestone # record shim; idempotency-test mod.
|
|
2055
2065
|
insert_budget_milestone = _cctally_milestones.insert_budget_milestone # record shim
|
|
2066
|
+
insert_projected_milestone = _cctally_milestones.insert_projected_milestone # record shim
|
|
2067
|
+
_projected_levels_already_latched = _cctally_milestones._projected_levels_already_latched # record shim
|
|
2056
2068
|
_reconcile_budget_milestones_on_set = _cctally_milestones._reconcile_budget_milestones_on_set # test_budget_alerts ns[]
|
|
2057
2069
|
_reconcile_budget_on_config_write = _cctally_milestones._reconcile_budget_on_config_write # forecast/config/dashboard c.; test_forecast_ns_patch mod. patch
|
|
2058
2070
|
|