cctally 1.20.3 → 1.20.4

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,11 @@ based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [1.20.4] - 2026-05-28
9
+
10
+ ### Changed
11
+ - **Hardened the v1.20.3 canonical-anchor bypass in `_select_non_overlapping_recorded_windows`: provenance is now passed explicitly from the loader instead of being inferred from merged weight.** v1.20.3 identified canonical anchors as `items[*].weight >= 1000`, treating the loader's `+1000` overlay as a provenance proxy. But the merged weight is `raw_snapshot_count + (1000 if canonical else 0)`, so a raw-only bucket can also reach 1000+ under bulk-imported history or any future polling-frequency change to `record-usage`. Two such raw-only buckets <5h apart would both force-restore through the bypass and render as overlapping recorded blocks. The selector now takes an explicit `canonical_anchors` keyword set, populated by `_load_recorded_five_hour_windows` from `set(canonical_intervals.keys()) - truncated_anchors` — the loader already knows which anchors came from `five_hour_blocks`, so there's no need to re-derive provenance from weight. The DP still runs over the full item set (canonical + raw) so non-canonical phantoms adjacent to a canonical anchor in a different floor bucket continue to lose by weight comparison; the force-restore step only puts back items the caller marked canonical, never any raw-only bucket regardless of its weight. `_CANONICAL_WEIGHT_THRESHOLD = 1000` stays as the loader's overlay value (still names what dominates the DP arithmetic). No user-visible behavior change under current production data — the v1.20.3 fix already worked correctly for every real input shape we've observed — but the contract is no longer fragile to changes in polling cadence or future bulk-import surfaces. Regression: new `test_select_non_overlapping_recorded_windows_high_weight_raw_does_not_bypass` covers two raw-only items at weights 1500/1200; pre-fix returned both, post-fix DP arbitrates to one. (#116 review follow-up)
12
+
8
13
  ## [1.20.3] - 2026-05-28
9
14
 
10
15
  ### Fixed
package/bin/cctally CHANGED
@@ -3951,17 +3951,23 @@ def _resolve_block_selector(
3951
3951
  return dict(row) if row else None
3952
3952
 
3953
3953
 
3954
- # Items in `_select_non_overlapping_recorded_windows` whose weight is
3955
- # >= this threshold are treated as canonical (sourced from
3956
- # ``five_hour_blocks``) and bypass the 5h non-overlap constraint. Must
3957
- # stay in sync with the ``counts[snapped] = counts.get(snapped, 0) + 1000``
3958
- # overlay in ``_load_recorded_five_hour_windows`` same constant, same
3959
- # meaning. Issue #116.
3954
+ # Weight overlay applied per canonical (``five_hour_blocks``) row by
3955
+ # ``_load_recorded_five_hour_windows``: ``counts[snapped] += _CANONICAL_WEIGHT_THRESHOLD``.
3956
+ # Gives canonical anchors dominant weight inside the
3957
+ # ``_select_non_overlapping_recorded_windows`` DP, so any non-canonical
3958
+ # phantom adjacent to a canonical anchor loses on weight comparison. NOT
3959
+ # used as a provenance check — the selector takes an explicit
3960
+ # ``canonical_anchors`` set from the loader for the force-restore bypass
3961
+ # (issue #116 review follow-up: raw-only buckets with bulk-imported /
3962
+ # high-frequency snapshot histories can also accumulate >= 1000 weight,
3963
+ # so the threshold conflates provenance with support count).
3960
3964
  _CANONICAL_WEIGHT_THRESHOLD = 1000
3961
3965
 
3962
3966
 
3963
3967
  def _select_non_overlapping_recorded_windows(
3964
3968
  items: list[tuple[dt.datetime, int]],
3969
+ *,
3970
+ canonical_anchors: set[dt.datetime] | None = None,
3965
3971
  ) -> list[dt.datetime]:
3966
3972
  """Pick the max-weight subset of recorded ``R`` values that respect
3967
3973
  the 5h non-overlap constraint, with canonical anchors guaranteed
@@ -3978,9 +3984,8 @@ def _select_non_overlapping_recorded_windows(
3978
3984
  snapshots: the subset that maximizes total support wins. Tie-break
3979
3985
  in the take branch favors including more ``R`` values.
3980
3986
 
3981
- Canonical bypass (issue #116): any item with weight at least
3982
- ``_CANONICAL_WEIGHT_THRESHOLD`` came from the authoritative
3983
- ``five_hour_blocks`` rollup (the caller overlays at +1000).
3987
+ Canonical bypass (issue #116): any ``R`` passed in ``canonical_anchors``
3988
+ came from the authoritative ``five_hour_blocks`` rollup.
3984
3989
  ``maybe_update_five_hour_block`` already deduped via
3985
3990
  ``_canonical_5h_window_key`` pre-insert, so two canonical rows are
3986
3991
  by definition non-overlapping physically — they only appear "in
@@ -3992,11 +3997,18 @@ def _select_non_overlapping_recorded_windows(
3992
3997
  a genuinely-adjacent block pair). The DP still runs over the full
3993
3998
  item set so non-canonical phantoms next to a canonical anchor get
3994
3999
  dropped by weight comparison; the canonical-bypass only force-
3995
- restores canonical items the DP dropped, never adds back a raw-
3996
- only phantom.
4000
+ restores anchors the caller marked canonical, never adds back a
4001
+ raw-only phantom (even one whose raw weight ≥ ``_CANONICAL_WEIGHT_THRESHOLD``
4002
+ — the v1.20.3 fix used weight as a provenance proxy, which the
4003
+ review correctly flagged as conflating support count with provenance).
3997
4004
 
3998
4005
  Args:
3999
4006
  items: ``(R, support_count)`` pairs.
4007
+ canonical_anchors: explicit set of ``R`` values sourced from
4008
+ ``five_hour_blocks``. Any present in ``items`` is guaranteed to
4009
+ appear in the result, even if the DP dropped it on the 5h
4010
+ non-overlap constraint. ``None`` / empty set = pure DP behavior
4011
+ (no bypass).
4000
4012
 
4001
4013
  Returns:
4002
4014
  Sorted ascending list of selected ``R`` values.
@@ -4039,11 +4051,14 @@ def _select_non_overlapping_recorded_windows(
4039
4051
  else:
4040
4052
  i -= 1
4041
4053
  chosen.reverse()
4042
- # Canonical bypass: force-restore any canonical-weight anchor the DP
4043
- # dropped (issue #116). Keep the result sorted ascending.
4044
- canonical = {R for R, w in items_sorted if w >= _CANONICAL_WEIGHT_THRESHOLD}
4045
- if canonical and not canonical.issubset(chosen):
4046
- return sorted(set(chosen) | canonical)
4054
+ # Canonical bypass: force-restore any canonical anchor the DP dropped
4055
+ # (issue #116). Intersect with items' keys so a caller passing anchors
4056
+ # outside the item set can't corrupt the result.
4057
+ if canonical_anchors:
4058
+ items_keys = {R for R, _ in items_sorted}
4059
+ present_canonical = canonical_anchors & items_keys
4060
+ if present_canonical and not present_canonical.issubset(chosen):
4061
+ return sorted(set(chosen) | present_canonical)
4047
4062
  return chosen
4048
4063
 
4049
4064
 
@@ -4334,8 +4349,16 @@ def _load_recorded_five_hour_windows(
4334
4349
  non_truncated_items = [
4335
4350
  (a, w) for a, w in counts.items() if a not in truncated_anchors
4336
4351
  ]
4352
+ # Pass canonical provenance explicitly: every key currently in
4353
+ # canonical_intervals came from a `five_hour_blocks` row (raw-only
4354
+ # buckets never land in this map). Subtract truncated_anchors because
4355
+ # those bypass the DP via the separate merge below — keeping them
4356
+ # out of canonical_anchors here is a no-op for correctness but
4357
+ # mirrors the same scope as non_truncated_items for clarity.
4358
+ canonical_anchors_for_dp = set(canonical_intervals.keys()) - truncated_anchors
4337
4359
  selected_non_truncated = _select_non_overlapping_recorded_windows(
4338
- non_truncated_items
4360
+ non_truncated_items,
4361
+ canonical_anchors=canonical_anchors_for_dp,
4339
4362
  )
4340
4363
  # Merge truncated anchors back in, sorted ascending. Their non-
4341
4364
  # overlap with the surrounding canonical blocks is guaranteed by
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cctally",
3
- "version": "1.20.3",
3
+ "version": "1.20.4",
4
4
  "description": "Claude Code usage tracker and local dashboard for Pro/Max subscription limits - weekly cost-per-percent trend, quota forecasts, threshold alerts. ccusage-compatible.",
5
5
  "homepage": "https://github.com/omrikais/cctally",
6
6
  "repository": {