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 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
  )
@@ -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
- if median is not None and (median - row.cache_hit_percent) >= threshold_pp:
800
- reasons.append("cache_drop")
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