cctally 1.10.3 → 1.11.1
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 +22 -0
- package/bin/_cctally_alerts.py +14 -18
- package/bin/_cctally_cache.py +28 -29
- package/bin/_cctally_cache_report.py +938 -0
- package/bin/_cctally_config.py +31 -22
- package/bin/_cctally_core.py +94 -8
- package/bin/_cctally_dashboard.py +621 -7
- package/bin/_cctally_db.py +42 -30
- package/bin/_cctally_record.py +26 -26
- package/bin/_cctally_setup.py +28 -26
- package/bin/_cctally_tui.py +47 -1
- package/bin/_cctally_update.py +41 -33
- package/bin/_lib_changelog.py +3 -1
- package/bin/_lib_share_templates.py +31 -13
- package/bin/cctally +214 -495
- package/dashboard/static/assets/index-BJ16SzRL.js +18 -0
- package/dashboard/static/assets/index-C1xH9GBW.css +1 -0
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +2 -1
- package/dashboard/static/assets/index-Cy59E7Ru.js +0 -18
- package/dashboard/static/assets/index-Dp14ELVt.css +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,28 @@ based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [1.11.1] - 2026-05-22
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- Promote 23 path globals (`APP_DIR`, `LEGACY_APP_DIR`, `LOG_DIR`, `DB_PATH`, `CACHE_DB_PATH`, `CACHE_LOCK_PATH`, `CACHE_LOCK_CODEX_PATH`, `CONFIG_PATH`, `CONFIG_LOCK_PATH`, `MIGRATION_ERROR_LOG_PATH`, `CHANGELOG_PATH`, all five `HOOK_TICK_*` paths, all six `UPDATE_*` paths, and `CLAUDE_SETTINGS_PATH`) from `bin/cctally` into `bin/_cctally_core.py`; `_cctally_core` is now the single source of truth and the single legal monkeypatch target for these names. `bin/cctally` keeps eager re-exports so external `cctally.X` references (ad-hoc REPL, scripts) still resolve, but internal `bin/cctally` reads (~28 sites) and every `bin/_*.py` sibling read (149 sites across 11 siblings) now go through `_cctally_core.X` at call time. `tests/conftest.py:redirect_paths()` patches `_cctally_core` directly (with an `ns["X"]` introspection mirror); the 80 historical `monkeypatch.setitem(ns, "<NAME>", v)` / `monkeypatch.setattr(cctally, "<NAME>", v)` test sites are migrated to `monkeypatch.setattr(_cctally_core, "<NAME>", v)`. `bin/_cctally_alerts.py` drops its `_cctally()` accessor helper (its only data-global use was `LOG_DIR`); other siblings keep the helper for legitimate non-data deps (validators, threshold constants, `PUBLIC_REPO`, `RELEASE_HEADER_RE`, etc.). The 2026-05-17 kernel-extraction spec's §2.3 path-constant accessor carve-out is retired. `tests/test_kernel_extraction_invariants.py` gains four new AST regression guards that lock the new invariants going forward (`test_promoted_globals_live_in_core`, `test_no_sibling_accessor_reads_promoted_globals`, `test_no_old_style_test_patches_for_promoted_globals`, `test_no_value_imports_of_promoted_globals_in_siblings`). (#84)
|
|
12
|
+
|
|
13
|
+
## [1.11.0] - 2026-05-22
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
- Dashboard: new Cache Report panel and modal exposing the `cctally cache-report` anomaly + savings surface as an always-on watchdog — accent-teal border by default, flips to accent-amber when today crosses an anomaly trigger (`cache_drop` ≥ 15pp drop vs the 14-day median or `net_negative` (`net_usd < 0`)), with a 14-day cache hit % mini-sparkline and a 7-day "+$X saved · N ⚠ days" subline; click the panel for a modal with six sections in order — today's spotlight (status pill + inline stat row + reasons line on anomaly), the full 14-day cache hit % timeline (`CacheSparkline` large variant with a ±5pp tinted baseline band), per-day net $ stacked bars (saved-green + thin red wasted-segment on positive days, downward amber bar on net-negative days), a counterfactual callout ("without caching, you'd have paid +$X more · cache efficiency Y%"), the daily rows table with per-column header colorization (date neutral · cache % cyan · saved green · wasted red · net purple · flag amber), and by-project (magenta) + by-model (blue) breakdown sub-cards. Panel sits at the tail of `DEFAULT_PANEL_ORDER` (position 11) on fresh installs; existing users with a saved `panelOrder` get the panel appended by the existing `reconcilePanelOrder` migration. No new third-party dependencies (hand-rolled SVG for the timeline + bar chart). Envelope is additive + nullable on the `cache_report` field — `envelope_version` stays at 2.
|
|
17
|
+
- Dashboard: `POST /api/settings` now accepts a `cache_report` block with key `anomaly_threshold_pp` (integer 1-100, default 15) — the modal's gear-icon settings popover round-trips through this endpoint and re-evaluates anomalies on the next sync. Invalid values return HTTP 400 with `{error, field}` (matching the existing settings-block validator convention); the popover surfaces the error inline under the input. The CLI's `cctally cache-report --anomaly-threshold-pp` flag is independent in v1 and continues to use its own default; the new `cache_report.anomaly_threshold_pp` config applies only to the dashboard sync path.
|
|
18
|
+
- CSS: new `--accent-teal` design token (the 11th panel accent color) plus `.panel.accent-teal` rule following the same shape as the other nine accent variants, and a `.crm-*` namespace for the Cache Report modal chrome (spotlight sub-card, chart frames, counterfactual callout, daily-rows table with per-column header accents, breakdown sub-cards, settings popover). Pickup follow-up: the panel's mobile collapse (`< 720px`) is now driven by a `useIsMobile()` hook applying the `cache-report-collapsed` modifier class (Implementor B added the CSS rule but left the JS toggle unwired).
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- CLI: `cctally cache-report` now buckets daily rows by the resolved `display.tz` instead of host-local. The pre-existing aggregator at `bin/cctally:2311` correctly resolved `display.tz` for `--since`/`--until` parsing but did NOT pass it to the day-bucketing path, so `--tz America/Los_Angeles` (or any non-host tz) shifted window edges without shifting the day-bucket dates rendered in the table. Threading an explicit `display_tz: ZoneInfo | None` parameter through `_aggregate_cache_by_day` closes the gap. Host-local hosts (the harness convention `TZ=Etc/UTC`) see no goldens change; non-UTC hosts may see day-bucket date strings shift by a day at midnight boundaries. Goldens regenerated via `bin/build-cache-report-fixtures.py`; aggregate per-day USD values are unchanged.
|
|
22
|
+
- HelpOverlay: panels at position 11+ now render an em-dash for the keybind cell instead of a literal `'11'` `<kbd>` label. The digit hotkeys at `main.tsx:62-74` only route to positions 1-10 of the active `panelOrder`; rendering `String(idx + 1)` for `idx > 8` previously emitted a `'11'` chip that wasn't bound to anything. Positional rule, not panel-id-specific — whatever panel sits at position 11 (Cache Report by default; any panel after drag-reorder) renders the em-dash.
|
|
23
|
+
- Dashboard Cache Report: panel chrome (border accent, header text color, "⚠ Today" header badge, sparkline today-marker color) no longer flips to amber during the baseline-building window (first 1–4 captured days). The server-side classifier can fire `net_negative` without a baseline, so `cr.today.anomaly_triggered` arrives true while `baseline_daily_row_count < CACHE_REPORT_MIN_BASELINE_DAYS` (5), but the watchdog is supposed to stay neutral until the 5-day floor exists. A new `chromeAmber = anomaly_triggered && !insufficient` gate at `CacheReportPanel.tsx` decouples the panel chrome from the headline copy — the modal's `.modal-card` accent mirrors the same gate so the panel-to-modal handoff stays teal during baseline-building. Round-2 Codex review finding (issue #77).
|
|
24
|
+
- Dashboard Cache Report: by-project / by-model breakdown cards now aggregate only over the same calendar dates as the displayed 14-day table. The kernel's rolling window can emit 15 distinct display-tz buckets when `now_utc - 14d` and `now_utc` straddle midnight (any non-UTC `display.tz`); `days` was already sliced to `window_days` in `bin/_cctally_dashboard.py`, but the breakdown inputs (raw `entries` for by-project; `result.rows` for by-model) were not — the cards silently included the dropped oldest bucket and stopped reconciling against the visible table / `CacheNetBars`. A `kept_dates = {r.date for r in days}` filter on both sides closes the gap; regression at `tests/test_dashboard_envelope_cache_report.py::test_build_cache_report_snapshot_breakdowns_match_days_window`. Round-2 Codex review finding (issue #77).
|
|
25
|
+
- Dashboard Cache Report modal: the daily-rows table's "Cache %" cell color now tracks each row's own `anomaly_reasons` instead of recomputing every row against today's `baseline_median_percent`. The server-side per-row classifier (`_cctally_cache_report._classify_anomalies`) uses `exclude_row=row` so each row has its own baseline; a window with five early 80% days followed by many 50% days kept the first 50% rows flagged `cache_drop` against the 80% reference, but the previous predicate (`d.cache_hit_percent < today.baseline_median_percent - CACHE_REPORT_BAND_PP`) painted them `hit-good` once today's median also drifted to 50% — green cells next to a ⚠ Flag column on the same row. Switching to `d.anomaly_reasons.includes('cache_drop')` keeps the cell color and the Flag column in lock-step regardless of how today's baseline moves; the `baselineKnown` neutral guard for the cold-start window is preserved. Round-2 Codex review finding (issue #77).
|
|
26
|
+
- Dashboard Cache Report modal: spotlight pill, sub-card border, and reasons line no longer flip to amber `⚠ Anomaly` during the baseline-building window (first 1–4 captured days). The server-side classifier fires `net_negative` without a baseline so `cr.today.anomaly_triggered` arrives true while `baseline_daily_row_count < CACHE_REPORT_MIN_BASELINE_DAYS=5`, but the panel and the outer modal-card chrome already gate the amber flip behind `!insufficient`. `CacheReportSpotlight` checked `anomaly_triggered` before `insufficient`, so the spotlight contradicted the panel and modal-card by rendering ⚠ Anomaly on the same day the rest of the surface said "Building baseline". The pill precedence now puts `insufficient` first, mirroring the gate at `CacheReportPanel.tsx:147` and `CacheReportModal.tsx:111`. Round-3 Codex review finding.
|
|
27
|
+
- Dashboard Cache Report modal settings: the anomaly-threshold popover now rejects fractional input (e.g. `1.5`, `15.9`, `1.0`, `15e2`) inline instead of silently truncating via `parseInt(value, 10)` and POSTing a different value than the user typed. The Save handler trims the input and matches against `/^-?\d+$/` before parsing, so any non-integer literal fails the existing `Must be an integer between 1 and 100` guard. `type="number"` does not block fractional typing/pasting on its own, and only the server previously rejected fractional values. Round-3 Codex review finding.
|
|
28
|
+
- Dashboard Cache Report modal: daily-rows "Cache %" cell color now tracks the same ±`CACHE_REPORT_BAND_PP`=5 band the sparkline draws around today's baseline median, replacing the round-2 binding to `anomaly_reasons.includes('cache_drop')`. The previous binding used the configurable `anomaly_threshold_pp` (default 15) for cell color, so any day 6-14pp below baseline rendered green even though it visibly sat outside the sparkline's highlighted band; raising the configured threshold widened the gap further. The Flag column (`flag-warn` / `flag-ok`) stays bound to each row's own `anomaly_triggered`, so the two signals — display-band coloring vs. per-row anomaly classifier — now stay independent and each carries its own meaning. Round-3 Codex review finding.
|
|
29
|
+
|
|
8
30
|
## [1.10.3] - 2026-05-21
|
|
9
31
|
|
|
10
32
|
### Fixed
|
package/bin/_cctally_alerts.py
CHANGED
|
@@ -24,11 +24,11 @@ in `bin/_lib_alerts_payload.py` (Phase A extraction); this module
|
|
|
24
24
|
imports them directly via `_load_lib`, which keeps the dispatch path
|
|
25
25
|
free of an extra bounce through cctally's re-exports.
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
Kernel reads from `bin/_cctally_core` (call-time module-attribute access):
|
|
28
|
+
- `LOG_DIR` — base dir under which `alerts.log` lives. Promoted to
|
|
29
|
+
`_cctally_core` 2026-05-22 (#84); test fixtures redirect via
|
|
30
|
+
`monkeypatch.setattr(_cctally_core, "LOG_DIR", tmp)` (or the
|
|
31
|
+
conftest `redirect_paths()` helper).
|
|
32
32
|
- `now_utc_iso` — single timestamp source used for both the log-line
|
|
33
33
|
timestamp and the synthetic test payload's `crossed_at_utc`.
|
|
34
34
|
|
|
@@ -50,10 +50,7 @@ import pathlib
|
|
|
50
50
|
import subprocess
|
|
51
51
|
import sys
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
def _cctally():
|
|
55
|
-
"""Resolve the current `cctally` module at call-time (spec §5.5)."""
|
|
56
|
-
return sys.modules["cctally"]
|
|
53
|
+
import _cctally_core
|
|
57
54
|
|
|
58
55
|
|
|
59
56
|
def _load_lib(name: str):
|
|
@@ -77,22 +74,21 @@ _build_alert_payload_five_hour = _lib_alerts_payload._build_alert_payload_five_h
|
|
|
77
74
|
|
|
78
75
|
|
|
79
76
|
# === Honest imports from extracted homes ===================================
|
|
80
|
-
# Spec 2026-05-17
|
|
81
|
-
#
|
|
82
|
-
#
|
|
83
|
-
#
|
|
77
|
+
# Spec 2026-05-17 §3.3: kernel symbols import from _cctally_core.
|
|
78
|
+
# LOG_DIR was promoted to _cctally_core 2026-05-22 (#84) and is read
|
|
79
|
+
# via call-time module-attribute access (this sibling no longer needs
|
|
80
|
+
# the historical _cctally() accessor).
|
|
84
81
|
from _cctally_core import now_utc_iso
|
|
85
82
|
|
|
86
83
|
|
|
87
84
|
def _alerts_log_path() -> "pathlib.Path":
|
|
88
85
|
"""Return ``~/.local/share/cctally/logs/alerts.log`` (parent dirs created).
|
|
89
86
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
transparently relocates the log without a separate env-var convention.
|
|
87
|
+
Reads ``LOG_DIR`` from ``_cctally_core`` at call time. Tests patch via
|
|
88
|
+
``monkeypatch.setattr(_cctally_core, "LOG_DIR", tmp)`` (or the
|
|
89
|
+
conftest ``redirect_paths()`` helper).
|
|
94
90
|
"""
|
|
95
|
-
log_dir =
|
|
91
|
+
log_dir = _cctally_core.LOG_DIR
|
|
96
92
|
log_dir.mkdir(parents=True, exist_ok=True)
|
|
97
93
|
return log_dir / "alerts.log"
|
|
98
94
|
|
package/bin/_cctally_cache.py
CHANGED
|
@@ -41,21 +41,18 @@ Holds:
|
|
|
41
41
|
- ``cmd_cache_sync`` — entry point for ``cctally cache-sync
|
|
42
42
|
[--source {claude,codex,all}] [--rebuild]``.
|
|
43
43
|
|
|
44
|
-
What
|
|
44
|
+
What lives in bin/_cctally_core (promoted 2026-05-22, #84):
|
|
45
45
|
- Path constants ``APP_DIR``, ``CACHE_DB_PATH``, ``CACHE_LOCK_PATH``,
|
|
46
|
-
``CACHE_LOCK_CODEX_PATH
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
``
|
|
50
|
-
``
|
|
51
|
-
``
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
``
|
|
56
|
-
widely scattered (record-usage tick, dashboard panels, share render
|
|
57
|
-
kernel, block tests, every JSONL-reading subcommand fixture) and
|
|
58
|
-
Phase C-style inline patching would touch dozens of sites.
|
|
46
|
+
``CACHE_LOCK_CODEX_PATH``. Moved bodies read these via call-time
|
|
47
|
+
``_cctally_core.X`` and tests patch via
|
|
48
|
+
``monkeypatch.setattr(_cctally_core, "X", v)`` (or the conftest
|
|
49
|
+
``redirect_paths()`` helper). The legacy
|
|
50
|
+
``setitem(ns, "CACHE_DB_PATH", …)`` pattern is forbidden by
|
|
51
|
+
``test_no_old_style_test_patches_for_promoted_globals``.
|
|
52
|
+
|
|
53
|
+
What stays in bin/cctally:
|
|
54
|
+
- ``CODEX_SESSIONS_DIR`` — out of scope for #84; still read via the
|
|
55
|
+
``c = _cctally()`` call-time accessor (spec §5.5).
|
|
59
56
|
- ``_sum_cost_for_range`` — sits at the cache↔report boundary; 6+
|
|
60
57
|
callers outside cache (forecast, weekly, report, project, doctor),
|
|
61
58
|
so the directive keeps it on the bin/cctally side.
|
|
@@ -122,6 +119,7 @@ def _cctally():
|
|
|
122
119
|
# Spec 2026-05-17-cctally-core-kernel-extraction.md §3.3: kernel symbols
|
|
123
120
|
# (Z-leaf + Z-mid) import from _cctally_core. The legacy shim function
|
|
124
121
|
# for ``eprint`` is deleted.
|
|
122
|
+
import _cctally_core
|
|
125
123
|
from _cctally_core import eprint
|
|
126
124
|
|
|
127
125
|
|
|
@@ -175,11 +173,12 @@ _CACHE_MIGRATIONS = _cctally_db_sib._CACHE_MIGRATIONS
|
|
|
175
173
|
|
|
176
174
|
|
|
177
175
|
# === BEGIN MOVED REGIONS ===
|
|
178
|
-
# Path constants
|
|
179
|
-
# CACHE_LOCK_CODEX_PATH
|
|
180
|
-
#
|
|
181
|
-
#
|
|
182
|
-
# in
|
|
176
|
+
# Path constants APP_DIR / CACHE_DB_PATH / CACHE_LOCK_PATH /
|
|
177
|
+
# CACHE_LOCK_CODEX_PATH live in _cctally_core (promoted 2026-05-22, #84);
|
|
178
|
+
# moved bodies read them via call-time ``_cctally_core.X`` and tests
|
|
179
|
+
# patch via ``monkeypatch.setattr(_cctally_core, "X", v)``.
|
|
180
|
+
# CODEX_SESSIONS_DIR stays in bin/cctally (out of scope for #84) and is
|
|
181
|
+
# still accessed via the ``c = _cctally()`` call-time accessor.
|
|
183
182
|
|
|
184
183
|
# === Region 1: ProjectKey + _resolve_project_key (was bin/cctally:1994-2069) ===
|
|
185
184
|
|
|
@@ -422,10 +421,10 @@ def sync_cache(
|
|
|
422
421
|
"""
|
|
423
422
|
stats = IngestStats()
|
|
424
423
|
c = _cctally()
|
|
425
|
-
|
|
426
|
-
|
|
424
|
+
_cctally_core.APP_DIR.mkdir(parents=True, exist_ok=True)
|
|
425
|
+
_cctally_core.CACHE_LOCK_PATH.touch()
|
|
427
426
|
|
|
428
|
-
lock_fh = open(
|
|
427
|
+
lock_fh = open(_cctally_core.CACHE_LOCK_PATH, "w")
|
|
429
428
|
try:
|
|
430
429
|
try:
|
|
431
430
|
fcntl.flock(lock_fh, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
@@ -914,10 +913,10 @@ def sync_codex_cache(
|
|
|
914
913
|
"""
|
|
915
914
|
stats = CodexIngestStats()
|
|
916
915
|
c = _cctally()
|
|
917
|
-
|
|
918
|
-
|
|
916
|
+
_cctally_core.APP_DIR.mkdir(parents=True, exist_ok=True)
|
|
917
|
+
_cctally_core.CACHE_LOCK_CODEX_PATH.touch()
|
|
919
918
|
|
|
920
|
-
lock_fh = open(
|
|
919
|
+
lock_fh = open(_cctally_core.CACHE_LOCK_CODEX_PATH, "w")
|
|
921
920
|
try:
|
|
922
921
|
try:
|
|
923
922
|
fcntl.flock(lock_fh, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
@@ -1263,17 +1262,17 @@ def open_cache_db() -> sqlite3.Connection:
|
|
|
1263
1262
|
recreated — the cache is fully re-derivable from JSONL, so this is safe.
|
|
1264
1263
|
"""
|
|
1265
1264
|
c = _cctally()
|
|
1266
|
-
|
|
1265
|
+
_cctally_core.APP_DIR.mkdir(parents=True, exist_ok=True)
|
|
1267
1266
|
try:
|
|
1268
|
-
conn = sqlite3.connect(
|
|
1267
|
+
conn = sqlite3.connect(_cctally_core.CACHE_DB_PATH)
|
|
1269
1268
|
conn.execute("SELECT 1").fetchone()
|
|
1270
1269
|
except sqlite3.DatabaseError as exc:
|
|
1271
1270
|
eprint(f"[cache] corrupt cache DB ({exc}); recreating")
|
|
1272
1271
|
try:
|
|
1273
|
-
|
|
1272
|
+
_cctally_core.CACHE_DB_PATH.unlink()
|
|
1274
1273
|
except FileNotFoundError:
|
|
1275
1274
|
pass
|
|
1276
|
-
conn = sqlite3.connect(
|
|
1275
|
+
conn = sqlite3.connect(_cctally_core.CACHE_DB_PATH)
|
|
1277
1276
|
|
|
1278
1277
|
conn.execute("PRAGMA journal_mode=WAL")
|
|
1279
1278
|
conn.execute("PRAGMA busy_timeout=5000")
|