cctally 1.89.0 → 1.89.2
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 +16 -0
- package/bin/_cctally_dashboard_cache_report.py +31 -0
- package/bin/_lib_cache_report.py +19 -2
- package/dashboard/static/assets/{index-B2exY5DG.js → index-BgoYXdus.js} +52 -52
- package/dashboard/static/assets/index-Ub8vwz1M.css +1 -0
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +1 -1
- package/dashboard/static/assets/index-DsXnVi9J.css +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,22 @@ based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [1.89.2] - 2026-07-31
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- Internal (maintainer-only): the public repository's CI matrix no longer fails on a test that requires maintainer-local tooling to be present. v1.89.1 stopped the remote-test transport harness from being treated as mandatory on a public checkout, but a test in the shipped suite still asserted that the same harness appears in a planned full-suite run — so the public matrix moved from failing before any test ran to failing on that one test, again across all three Python versions. That assertion now applies only in the private checkout, keyed off the very marker the runner itself uses to decide the harness is required, so the public suite skips it and the private tree still asserts it in full. (#446)
|
|
12
|
+
|
|
13
|
+
## [1.89.1] - 2026-07-31
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- Cache Report no longer shows a healthy measured 0% on a day with no activity yet — the panel, spotlight, daily rows, sparkline and net-$ bars now mark an unmeasured day explicitly (#443).
|
|
17
|
+
- Cache Report daily rows whose anomaly predicates could not be evaluated now render a neutral flag with an explanatory legend, instead of a green check (#443).
|
|
18
|
+
- Cache Report single-source views now show a provider's degraded or stale status chip, matching what the all-sources view already showed (#443).
|
|
19
|
+
- The all-sources Cache Report summary no longer reports an anomaly for data the panel beside it calls "building baseline" (#443).
|
|
20
|
+
- An empty Cache Report source no longer renders KPIs and a healthy verdict alongside its empty label (#443).
|
|
21
|
+
- A Cache Report that failed to build now says so, instead of showing an indefinite loading state (#443).
|
|
22
|
+
- Internal (maintainer-only): the public repository's CI matrix no longer fails before running a single test. v1.89.0 started treating three harnesses as mandatory so that a lost executable bit could not silently drop one from a full-suite run, but one of them — the remote-test transport harness — is maintainer-local tooling that the public mirror deliberately never carries, so on every public checkout the guard refused immediately and took all three Python versions down with it. The requirement now applies only in the private checkout, where the file is supposed to exist, while the public matrix goes on running the shipped subset as designed. Nothing the guard was added to catch has been given up: in the private tree a harness that loses its executable bit — or disappears outright — still fails the run. (#446)
|
|
23
|
+
|
|
8
24
|
## [1.89.0] - 2026-07-31
|
|
9
25
|
|
|
10
26
|
### Fixed
|
|
@@ -135,6 +135,12 @@ class CacheReportDailyRow:
|
|
|
135
135
|
net_usd: float
|
|
136
136
|
anomaly_triggered: bool
|
|
137
137
|
anomaly_reasons: tuple[str, ...]
|
|
138
|
+
# #443 S1. `anomaly_unevaluated` = the predicates the classifier declined
|
|
139
|
+
# to run for this row; `observed` is False only for the builder's
|
|
140
|
+
# synthetic today row. Both default so their absence reproduces pre-S1
|
|
141
|
+
# rendering exactly.
|
|
142
|
+
anomaly_unevaluated: tuple[str, ...] = ()
|
|
143
|
+
observed: bool = True
|
|
138
144
|
|
|
139
145
|
|
|
140
146
|
@dataclass(frozen=True)
|
|
@@ -161,6 +167,9 @@ class CacheReportTodaySpotlight:
|
|
|
161
167
|
anomaly_triggered: bool
|
|
162
168
|
anomaly_reasons: tuple[str, ...]
|
|
163
169
|
baseline_daily_row_count: int
|
|
170
|
+
# #443 S1 — see CacheReportDailyRow.
|
|
171
|
+
anomaly_unevaluated: tuple[str, ...] = ()
|
|
172
|
+
observed: bool = True
|
|
164
173
|
|
|
165
174
|
|
|
166
175
|
def _cache_report_snapshot_to_dict(cr: "CacheReportSnapshot | None") -> "dict | None":
|
|
@@ -189,6 +198,8 @@ def _cache_report_snapshot_to_dict(cr: "CacheReportSnapshot | None") -> "dict |
|
|
|
189
198
|
"anomaly_triggered": cr.today.anomaly_triggered,
|
|
190
199
|
"anomaly_reasons": list(cr.today.anomaly_reasons),
|
|
191
200
|
"baseline_daily_row_count": cr.today.baseline_daily_row_count,
|
|
201
|
+
"anomaly_unevaluated": list(cr.today.anomaly_unevaluated),
|
|
202
|
+
"observed": cr.today.observed,
|
|
192
203
|
},
|
|
193
204
|
"days": [
|
|
194
205
|
{
|
|
@@ -203,6 +214,8 @@ def _cache_report_snapshot_to_dict(cr: "CacheReportSnapshot | None") -> "dict |
|
|
|
203
214
|
"net_usd": d.net_usd,
|
|
204
215
|
"anomaly_triggered": d.anomaly_triggered,
|
|
205
216
|
"anomaly_reasons": list(d.anomaly_reasons),
|
|
217
|
+
"anomaly_unevaluated": list(d.anomaly_unevaluated),
|
|
218
|
+
"observed": d.observed,
|
|
206
219
|
}
|
|
207
220
|
for d in cr.days
|
|
208
221
|
],
|
|
@@ -319,6 +332,12 @@ def _cache_report_empty(
|
|
|
319
332
|
):
|
|
320
333
|
"""The empty (no in-window entries) ``CacheReportSnapshot`` — factored so the
|
|
321
334
|
warm and cold builder paths share one ``is_empty`` return (#272 §6)."""
|
|
335
|
+
crk = _cache_report_load_kernel()
|
|
336
|
+
# #443 S1 — the dataclass defaults would claim ``observed=True`` with an
|
|
337
|
+
# empty ``anomaly_unevaluated`` for a day that was definitionally never
|
|
338
|
+
# measured or classified. That is the fabricating default this session
|
|
339
|
+
# removes everywhere else, so it is stated explicitly here too even though
|
|
340
|
+
# the empty branch short-circuits before any of it renders.
|
|
322
341
|
empty_today = CacheReportTodaySpotlight(
|
|
323
342
|
date=today_iso,
|
|
324
343
|
cache_hit_percent=0.0,
|
|
@@ -328,6 +347,8 @@ def _cache_report_empty(
|
|
|
328
347
|
anomaly_triggered=False,
|
|
329
348
|
anomaly_reasons=(),
|
|
330
349
|
baseline_daily_row_count=0,
|
|
350
|
+
anomaly_unevaluated=tuple(crk.CACHE_ANOMALY_PREDICATES),
|
|
351
|
+
observed=False,
|
|
331
352
|
)
|
|
332
353
|
return CacheReportSnapshot(
|
|
333
354
|
window_days=window_days,
|
|
@@ -555,6 +576,10 @@ def build_cache_report_snapshot(
|
|
|
555
576
|
anomaly_triggered=False,
|
|
556
577
|
anomaly_reasons=(),
|
|
557
578
|
baseline_daily_row_count=baseline_daily_row_count,
|
|
579
|
+
# This row never reaches the classifier, so nothing was evaluated
|
|
580
|
+
# for it and nothing was measured behind it (#443 S1 F1/F2).
|
|
581
|
+
anomaly_unevaluated=tuple(crk.CACHE_ANOMALY_PREDICATES),
|
|
582
|
+
observed=False,
|
|
558
583
|
)
|
|
559
584
|
else:
|
|
560
585
|
today_spotlight = CacheReportTodaySpotlight(
|
|
@@ -568,6 +593,8 @@ def build_cache_report_snapshot(
|
|
|
568
593
|
anomaly_triggered=today_row.anomaly_triggered,
|
|
569
594
|
anomaly_reasons=tuple(today_row.anomaly_reasons),
|
|
570
595
|
baseline_daily_row_count=baseline_daily_row_count,
|
|
596
|
+
anomaly_unevaluated=tuple(today_row.anomaly_unevaluated),
|
|
597
|
+
observed=True,
|
|
571
598
|
)
|
|
572
599
|
|
|
573
600
|
# Daily rows — newest first, capped at ``window_days``.
|
|
@@ -615,6 +642,8 @@ def build_cache_report_snapshot(
|
|
|
615
642
|
net_usd=0.0,
|
|
616
643
|
anomaly_triggered=False,
|
|
617
644
|
anomaly_reasons=(),
|
|
645
|
+
anomaly_unevaluated=tuple(crk.CACHE_ANOMALY_PREDICATES),
|
|
646
|
+
observed=False,
|
|
618
647
|
)
|
|
619
648
|
)
|
|
620
649
|
days_newest_first.extend(
|
|
@@ -630,6 +659,8 @@ def build_cache_report_snapshot(
|
|
|
630
659
|
net_usd=r.net_usd,
|
|
631
660
|
anomaly_triggered=r.anomaly_triggered,
|
|
632
661
|
anomaly_reasons=tuple(r.anomaly_reasons),
|
|
662
|
+
anomaly_unevaluated=tuple(r.anomaly_unevaluated),
|
|
663
|
+
observed=True,
|
|
633
664
|
)
|
|
634
665
|
for r in raw_days_newest_first
|
|
635
666
|
)
|
package/bin/_lib_cache_report.py
CHANGED
|
@@ -119,6 +119,11 @@ CACHE_REPORT_MIN_BASELINE_SESSIONS = 10
|
|
|
119
119
|
# dashboard/web/src/types/envelope.ts:71 — keeps the two surfaces in
|
|
120
120
|
# lockstep so a typo on either side fails type-check.
|
|
121
121
|
CacheAnomalyReason = Literal["net_negative", "cache_drop"]
|
|
122
|
+
# Every predicate _classify_anomalies can run, in reason-append order. The
|
|
123
|
+
# TypeScript mirror is CACHE_ANOMALY_PREDICATES in cacheReportVerdict.ts.
|
|
124
|
+
CACHE_ANOMALY_PREDICATES: tuple[CacheAnomalyReason, ...] = (
|
|
125
|
+
"net_negative", "cache_drop",
|
|
126
|
+
)
|
|
122
127
|
|
|
123
128
|
|
|
124
129
|
@dataclass
|
|
@@ -185,6 +190,10 @@ class CacheRow:
|
|
|
185
190
|
# Anomaly (populated by _classify_anomalies)
|
|
186
191
|
anomaly_triggered: bool = False
|
|
187
192
|
anomaly_reasons: list[CacheAnomalyReason] = field(default_factory=list)
|
|
193
|
+
# Predicates the classifier did NOT run for this row. Empty means every
|
|
194
|
+
# predicate was evaluated; a non-empty list is what lets the dashboard
|
|
195
|
+
# distinguish "evaluated, clean" from "never evaluated" (issue #443 F2).
|
|
196
|
+
anomaly_unevaluated: list[CacheAnomalyReason] = field(default_factory=list)
|
|
188
197
|
|
|
189
198
|
@property
|
|
190
199
|
def total_tokens(self) -> int:
|
|
@@ -767,6 +776,7 @@ def _classify_anomalies(
|
|
|
767
776
|
for row in rows:
|
|
768
777
|
row.anomaly_triggered = False
|
|
769
778
|
row.anomaly_reasons = []
|
|
779
|
+
row.anomaly_unevaluated = list(CACHE_ANOMALY_PREDICATES)
|
|
770
780
|
return
|
|
771
781
|
if not rows:
|
|
772
782
|
return
|
|
@@ -782,24 +792,31 @@ def _classify_anomalies(
|
|
|
782
792
|
|
|
783
793
|
for i, row in enumerate(rows):
|
|
784
794
|
reasons: list[CacheAnomalyReason] = []
|
|
795
|
+
unevaluated: list[CacheAnomalyReason] = []
|
|
785
796
|
|
|
786
797
|
# Trigger 1: net_negative (no baseline needed; cache-activity guard).
|
|
787
798
|
if row.cache_creation_tokens + row.cache_read_tokens > 0:
|
|
788
799
|
if row.net_usd < 0:
|
|
789
800
|
reasons.append("net_negative")
|
|
801
|
+
else:
|
|
802
|
+
unevaluated.append("net_negative")
|
|
790
803
|
|
|
791
804
|
# Trigger 2: cache_drop (requires baseline).
|
|
792
805
|
anchor = anchors[i]
|
|
806
|
+
median = None
|
|
793
807
|
if anchor is not None:
|
|
794
808
|
median = _compute_baseline_median(
|
|
795
809
|
rows, anchor=anchor,
|
|
796
810
|
window_days=window_days, min_samples=min_baseline,
|
|
797
811
|
exclude_row=row, is_session_mode=is_session_mode,
|
|
798
812
|
)
|
|
799
|
-
|
|
800
|
-
|
|
813
|
+
if median is None:
|
|
814
|
+
unevaluated.append("cache_drop")
|
|
815
|
+
elif (median - row.cache_hit_percent) >= threshold_pp:
|
|
816
|
+
reasons.append("cache_drop")
|
|
801
817
|
|
|
802
818
|
row.anomaly_reasons = reasons
|
|
819
|
+
row.anomaly_unevaluated = unevaluated
|
|
803
820
|
row.anomaly_triggered = bool(reasons)
|
|
804
821
|
|
|
805
822
|
|