cctally 1.7.2 → 1.7.3

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.
@@ -1030,6 +1030,56 @@ def _tui_build_percent_milestones(
1030
1030
  return out
1031
1031
 
1032
1032
 
1033
+ def _tui_build_five_hour_milestones(
1034
+ conn: sqlite3.Connection,
1035
+ five_hour_window_key: int | None,
1036
+ ) -> list[dict]:
1037
+ """Return per-percent 5h-block milestones for the given window, in
1038
+ capture-time order. Spec §5.3 — drives the CurrentWeekModal's new
1039
+ 5h-milestone timeline section.
1040
+
1041
+ Bucket B per §3.2: NO ``reset_event_id`` filter — both pre- and
1042
+ post-credit segments render in the merged chronological stream so
1043
+ the user sees the full history of the active block including
1044
+ repeated threshold values after an in-place credit. The React layer
1045
+ differentiates rows by ``reset_event_id`` for key uniqueness.
1046
+
1047
+ Returns [] when the current week has no API-anchored 5h block. The
1048
+ envelope-shaped dict mirrors the CLI ``five-hour-breakdown --json``
1049
+ milestone objects but with snake_case keys (envelope convention).
1050
+ """
1051
+ if five_hour_window_key is None:
1052
+ return []
1053
+ rows = conn.execute(
1054
+ """
1055
+ SELECT percent_threshold, captured_at_utc, block_cost_usd,
1056
+ marginal_cost_usd, seven_day_pct_at_crossing,
1057
+ reset_event_id
1058
+ FROM five_hour_milestones
1059
+ WHERE five_hour_window_key = ?
1060
+ ORDER BY captured_at_utc ASC, id ASC
1061
+ """,
1062
+ (int(five_hour_window_key),),
1063
+ ).fetchall()
1064
+ out: list[dict] = []
1065
+ for r in rows:
1066
+ out.append({
1067
+ "percent_threshold": int(r["percent_threshold"]),
1068
+ "captured_at_utc": r["captured_at_utc"],
1069
+ "block_cost_usd": float(r["block_cost_usd"]),
1070
+ "marginal_cost_usd": (
1071
+ None if r["marginal_cost_usd"] is None
1072
+ else float(r["marginal_cost_usd"])
1073
+ ),
1074
+ "seven_day_pct_at_crossing": (
1075
+ None if r["seven_day_pct_at_crossing"] is None
1076
+ else float(r["seven_day_pct_at_crossing"])
1077
+ ),
1078
+ "reset_event_id": int(r["reset_event_id"] or 0),
1079
+ })
1080
+ return out
1081
+
1082
+
1033
1083
  @dataclass
1034
1084
  class DataSnapshot:
1035
1085
  """All data needed to render one TUI frame. Produced by sync thread,
@@ -1062,6 +1112,13 @@ class DataSnapshot:
1062
1112
  # `current_week.five_hour_block` is precomputed via
1063
1113
  # `_select_current_block_for_envelope`).
1064
1114
  alerts: list[dict] = field(default_factory=list)
1115
+ # ---- 5h in-place credit (v1.7.x) ----
1116
+ # Already-envelope-shaped dicts for the CurrentWeekModal's new 5h
1117
+ # milestone timeline (spec §5.3, Codex r1 finding 3). Parallel to
1118
+ # ``percent_milestones`` (which carries the WEEKLY timeline). Loaded
1119
+ # at sync-thread time so ``snapshot_to_envelope`` stays a pure
1120
+ # renderer; empty list when no current 5h block is bound.
1121
+ five_hour_milestones: list[dict] = field(default_factory=list)
1065
1122
 
1066
1123
  @classmethod
1067
1124
  def synthesize_for_marketing(cls, *, as_of_iso: str) -> "DataSnapshot":
@@ -1888,6 +1945,19 @@ def _tui_build_snapshot(
1888
1945
  alerts = _build_alerts_envelope_array(conn)
1889
1946
  except Exception as exc:
1890
1947
  errors.append(f"alerts: {exc}")
1948
+ # ---- 5h in-place credit (v1.7.x) ----
1949
+ # Load 5h milestones (pre + post credit) for the current
1950
+ # block's window so CurrentWeekModal can render a merged
1951
+ # chronological timeline alongside its weekly milestones.
1952
+ # Spec §5.3 (Codex r1 finding 3).
1953
+ fh_milestones: list[dict] = []
1954
+ try:
1955
+ win_key = None
1956
+ if cw is not None and isinstance(cw.five_hour_block, dict):
1957
+ win_key = cw.five_hour_block.get("five_hour_window_key")
1958
+ fh_milestones = _tui_build_five_hour_milestones(conn, win_key)
1959
+ except Exception as exc:
1960
+ errors.append(f"five-hour-milestones: {exc}")
1891
1961
  return DataSnapshot(
1892
1962
  current_week=cw,
1893
1963
  forecast=fc,
@@ -1903,6 +1973,7 @@ def _tui_build_snapshot(
1903
1973
  blocks_panel=blocks_panel,
1904
1974
  daily_panel=daily_panel,
1905
1975
  alerts=alerts,
1976
+ five_hour_milestones=fh_milestones,
1906
1977
  )
1907
1978
  finally:
1908
1979
  conn.close()
@@ -2375,11 +2446,26 @@ def _tui_panel_current_week(
2375
2446
  f"${cw.dollars_per_percent:.2f}"
2376
2447
  if cw.dollars_per_percent is not None else "—"
2377
2448
  )
2449
+ # Spec §5.4 — credit badge next to the 5h percent. Source: same
2450
+ # ``cw.five_hour_block.credits`` channel that drives the dashboard
2451
+ # chip; only show when at least one credit is present for the
2452
+ # current block. Format: ``⚡ -Xpp`` (single) / ``⚡ -Xpp, -Ypp``
2453
+ # (stacked across distinct 10-min slots).
2454
+ fh_credit_badge = ""
2455
+ fhb = getattr(cw, "five_hour_block", None)
2456
+ if isinstance(fhb, dict):
2457
+ fh_credits = fhb.get("credits") or []
2458
+ if fh_credits:
2459
+ deltas = ", ".join(
2460
+ f"{float(c.get('delta_pp', 0.0)):+.0f}pp"
2461
+ for c in fh_credits
2462
+ )
2463
+ fh_credit_badge = f" {{bright}}⚡ {deltas}{{/}}"
2378
2464
  lines = [
2379
2465
  "",
2380
2466
  f" Used {{{used_cls}}}{bar_fill}{{/}} {{{used_cls}.b}}{cw.used_pct:>5.1f}%{{/}}",
2381
2467
  "",
2382
- f" 5-hour {{bar.accent}}{five_bar}{{/}} {{bright}}{int(five):>3d}%{{/}}",
2468
+ f" 5-hour {{bar.accent}}{five_bar}{{/}} {{bright}}{int(five):>3d}%{{/}}{fh_credit_badge}",
2383
2469
  f" {{dim}}{fr_str}{{/}}" if fr_str else "",
2384
2470
  "",
2385
2471
  f" {{dim}}Spent{{/}} {{bright}}${cw.spent_usd:.2f}{{/}} "
@@ -2428,6 +2514,19 @@ def _tui_panel_current_week_hero(
2428
2514
  else:
2429
2515
  reset_suffix = ""
2430
2516
 
2517
+ # Spec §5.4 — credit badge in the hero variant. Same source as the
2518
+ # grid variant; append after the reset suffix so the badge follows
2519
+ # the "resets in" timer.
2520
+ fhb_hero = getattr(cw, "five_hour_block", None)
2521
+ if isinstance(fhb_hero, dict):
2522
+ fh_credits_hero = fhb_hero.get("credits") or []
2523
+ if fh_credits_hero:
2524
+ deltas_hero = ", ".join(
2525
+ f"{float(c.get('delta_pp', 0.0)):+.0f}pp"
2526
+ for c in fh_credits_hero
2527
+ )
2528
+ reset_suffix = f"{reset_suffix} {{bright}}⚡ {deltas_hero}{{/}}"
2529
+
2431
2530
  if snap.last_sync_error:
2432
2531
  health = "{warn}daemon error{/}"
2433
2532
  elif snap.last_sync_at is None:
@@ -2671,6 +2671,12 @@ def _five_hour_blocks_to_json(
2671
2671
  "sevenDayPctAtBlockEnd": p_end,
2672
2672
  "sevenDayPctDeltaPp": delta,
2673
2673
  "crossedSevenDayReset": crossed,
2674
+ # Spec §5.1 — in-place credit events for this 5h block, in
2675
+ # ascending effective_reset_at order. Empty list when the
2676
+ # block carries no credit detections. Source: ``__credits``
2677
+ # side-channel attached by ``cmd_five_hour_blocks`` via
2678
+ # ``credits_by_window``.
2679
+ "credits": d.get("__credits") or [],
2674
2680
  }
2675
2681
  if breakdown_axis == "model":
2676
2682
  out["modelBreakdowns"] = [
@@ -2752,6 +2758,20 @@ def _render_five_hour_blocks_table(
2752
2758
  formatted_start = _format_block_start(d["block_start_at"], args._resolved_tz)
2753
2759
  if crossed:
2754
2760
  formatted_start = f"⚡ {formatted_start}"
2761
+ # Spec §5.1 — credit chip suffix. When the block carries one or
2762
+ # more in-place credit events (5h credit detection v1.7.x;
2763
+ # __credits side-channel set by ``cmd_five_hour_blocks``), append
2764
+ # a ⚡-prefixed chip listing the per-credit delta-pp. Multiple
2765
+ # credits in the same block concatenate (e.g.
2766
+ # `⚡ credited -20pp, -8pp`). The chip text sits AFTER the
2767
+ # block-start cell's existing crossed-reset glyph so the two
2768
+ # signals visually disambiguate by position (crossed-reset
2769
+ # prefix vs. credit suffix). Both use ⚡ — different semantics,
2770
+ # different positions.
2771
+ credits = d.get("__credits") or []
2772
+ if credits:
2773
+ deltas = ", ".join(f"{c['deltaPp']:+.0f}pp" for c in credits)
2774
+ formatted_start = f"{formatted_start} ⚡ credited {deltas}"
2755
2775
  rows.append([
2756
2776
  formatted_start,
2757
2777
  "ACTIVE" if d["__is_active"] else "closed",
package/bin/cctally CHANGED
@@ -3864,6 +3864,43 @@ def open_db() -> sqlite3.Connection:
3864
3864
  )
3865
3865
  _backfill_week_reset_events(conn)
3866
3866
 
3867
+ # ── five_hour_reset_events (Anthropic-issued in-place 5h credits) ──
3868
+ # Parallel concept to ``week_reset_events`` for the 5h dimension; lives
3869
+ # adjacent in ``_apply_schema`` because the two carry the same kind of
3870
+ # signal at different cadences. Diverges from weekly in that the payload
3871
+ # is the *percent values* (prior + post) rather than boundary keys,
3872
+ # because the 5h variant has a stable ``five_hour_window_key`` and only
3873
+ # the percent moves. See spec
3874
+ # docs/superpowers/specs/2026-05-16-5h-in-place-credit-detection.md §3.1
3875
+ # for rationale.
3876
+ #
3877
+ # UNIQUE(five_hour_window_key, effective_reset_at_utc) — supports stacked
3878
+ # credits across DISTINCT 10-min slots inside one block (see spec §2.3
3879
+ # "Bounded stacked-credit resolution" for the cap statement: ~30 distinct
3880
+ # slots per 5h block when floor matches ``_canonical_5h_window_key``'s
3881
+ # 600-second floor; same-slot collisions silently absorbed by
3882
+ # INSERT OR IGNORE — an intentional cap, not a bug).
3883
+ #
3884
+ # No FK per CLAUDE.md gotcha: FKs in this codebase are documentation-only
3885
+ # (``PRAGMA foreign_keys`` not enabled). ``five_hour_window_key`` provides
3886
+ # the join key without a formal FK.
3887
+ #
3888
+ # No ``_backfill_five_hour_reset_events`` call follows (forward-only ship
3889
+ # per spec Q5; historical backfill deferred to a future issue).
3890
+ conn.execute(
3891
+ """
3892
+ CREATE TABLE IF NOT EXISTS five_hour_reset_events (
3893
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
3894
+ detected_at_utc TEXT NOT NULL,
3895
+ five_hour_window_key INTEGER NOT NULL,
3896
+ prior_percent REAL NOT NULL,
3897
+ post_percent REAL NOT NULL,
3898
+ effective_reset_at_utc TEXT NOT NULL,
3899
+ UNIQUE(five_hour_window_key, effective_reset_at_utc)
3900
+ )
3901
+ """
3902
+ )
3903
+
3867
3904
  # ── five_hour_blocks (rollup, one row per API-anchored 5h block) ──
3868
3905
  conn.execute(
3869
3906
  """
@@ -3913,7 +3950,8 @@ def open_db() -> sqlite3.Connection:
3913
3950
  block_cost_usd REAL NOT NULL DEFAULT 0,
3914
3951
  marginal_cost_usd REAL,
3915
3952
  seven_day_pct_at_crossing REAL,
3916
- UNIQUE(five_hour_window_key, percent_threshold),
3953
+ reset_event_id INTEGER NOT NULL DEFAULT 0,
3954
+ UNIQUE(five_hour_window_key, percent_threshold, reset_event_id),
3917
3955
  FOREIGN KEY (block_id) REFERENCES five_hour_blocks(id)
3918
3956
  )
3919
3957
  """
@@ -3933,6 +3971,16 @@ def open_db() -> sqlite3.Connection:
3933
3971
  # — never "delivery failed".
3934
3972
  add_column_if_missing(conn, "five_hour_milestones", "alerted_at", "TEXT")
3935
3973
 
3974
+ # reset_event_id: segment column added by migration 006. Fresh-install
3975
+ # DBs get it via the live CREATE TABLE above + the dispatcher fast-stamps
3976
+ # the migration marker (the live DDL must carry the column AND the 3-col
3977
+ # UNIQUE for fast-stamp to be safe — see spec §3.2). Existing pre-006
3978
+ # DBs trip the migration's rename-recreate-copy idiom (handler in
3979
+ # bin/_cctally_db.py); the handler's fast-path probe stamps the marker
3980
+ # when the column is already present (covers the corner case where a
3981
+ # partially-upgraded DB has the column but not the new UNIQUE — re-run
3982
+ # is safe). Mirrors weekly migration 005 / `percent_milestones`.
3983
+
3936
3984
  # ── five_hour_block_models (per-(block, model) rollup-child) ──
3937
3985
  # MUST be created BEFORE the parent-backfill gate below, because
3938
3986
  # _backfill_five_hour_blocks writes into this table on the fresh-install
@@ -7632,6 +7680,14 @@ def _backfill_week_reset_events(conn: sqlite3.Connection) -> None:
7632
7680
  # 86→0 and 34→0 cases while filtering 35→33-style jitter.
7633
7681
  _RESET_PCT_DROP_THRESHOLD = 25.0
7634
7682
 
7683
+ # In-place 5h-credit threshold. Mirrors `_RESET_PCT_DROP_THRESHOLD` but
7684
+ # scaled down for the 5h dimension: typical 5h usage stays under ~10pp in
7685
+ # a single block, so a 5pp drop sits well above natural variation while
7686
+ # proportionally being a larger signal than 25pp is on the weekly scale.
7687
+ # See spec docs/superpowers/specs/2026-05-16-5h-in-place-credit-detection.md
7688
+ # §2.1 (Q1) for rationale.
7689
+ _FIVE_HOUR_RESET_PCT_DROP_THRESHOLD = 5.0
7690
+
7635
7691
 
7636
7692
  def _week_ref_has_reset_event(
7637
7693
  conn: sqlite3.Connection, ref: WeekRef
@@ -9457,6 +9513,31 @@ def cmd_five_hour_blocks(args: argparse.Namespace) -> int:
9457
9513
  # to fill seven_day_pct_at_block_end on the active row.
9458
9514
  latest_7d, latest_window_key = _latest_seven_day_and_window(conn)
9459
9515
 
9516
+ # Pre-load credit events for every window_key the rows query
9517
+ # returned. Single index-scan over `five_hour_reset_events`;
9518
+ # build a window_key -> list[Credit] map keyed for in-process
9519
+ # JOIN against each block dict. Used by both the text/JSON
9520
+ # render path AND the share-output snapshot wiring (spec §5.1.1).
9521
+ # Loaded in a single pass — no per-block SELECT.
9522
+ credit_rows = conn.execute(
9523
+ "SELECT five_hour_window_key, prior_percent, post_percent, "
9524
+ " effective_reset_at_utc "
9525
+ " FROM five_hour_reset_events "
9526
+ " ORDER BY five_hour_window_key, effective_reset_at_utc"
9527
+ ).fetchall()
9528
+ credits_by_window: dict[int, list[dict]] = {}
9529
+ for cr in credit_rows:
9530
+ credits_by_window.setdefault(
9531
+ int(cr["five_hour_window_key"]), []
9532
+ ).append({
9533
+ "effectiveResetAtUtc": cr["effective_reset_at_utc"],
9534
+ "priorPercent": float(cr["prior_percent"]),
9535
+ "postPercent": float(cr["post_percent"]),
9536
+ "deltaPp": round(
9537
+ float(cr["post_percent"]) - float(cr["prior_percent"]), 1
9538
+ ),
9539
+ })
9540
+
9460
9541
  # Build per-block dicts with the active-flag side-channel.
9461
9542
  block_dicts: list[dict] = []
9462
9543
  for r in rows:
@@ -9465,6 +9546,11 @@ def cmd_five_hour_blocks(args: argparse.Namespace) -> int:
9465
9546
  d["__is_active"] = is_active
9466
9547
  if is_active and latest_7d is not None:
9467
9548
  d["seven_day_pct_at_block_end"] = latest_7d
9549
+ # Side-channel (parallel to __is_active): list of credit
9550
+ # event dicts for this block's window. Empty list when none.
9551
+ d["__credits"] = credits_by_window.get(
9552
+ int(d["five_hour_window_key"]), []
9553
+ )
9468
9554
  block_dicts.append(d)
9469
9555
 
9470
9556
  # Shareable-reports gate: --format short-circuits the JSON / table
@@ -9578,18 +9664,50 @@ def cmd_five_hour_breakdown(args: argparse.Namespace) -> int:
9578
9664
  )
9579
9665
  return 2
9580
9666
 
9667
+ # Spec §5.2: ORDER BY captured_at_utc ASC (NOT percent_threshold)
9668
+ # so post-credit segments interleave with pre-credit ones in
9669
+ # time-order — same human threshold number can appear twice
9670
+ # (once per reset_event_id segment) and must render in the
9671
+ # order it crossed. Bucket B per §3.2: read ALL segments (no
9672
+ # ``reset_event_id`` filter).
9581
9673
  milestones = conn.execute(
9582
9674
  """
9583
9675
  SELECT percent_threshold, captured_at_utc,
9584
9676
  block_cost_usd, marginal_cost_usd,
9585
- seven_day_pct_at_crossing
9677
+ seven_day_pct_at_crossing, reset_event_id
9586
9678
  FROM five_hour_milestones
9587
9679
  WHERE block_id = ?
9588
- ORDER BY percent_threshold ASC
9680
+ ORDER BY captured_at_utc ASC, id ASC
9589
9681
  """,
9590
9682
  (block["id"],),
9591
9683
  ).fetchall()
9592
9684
 
9685
+ # Spec §5.2 — load in-place credit events for this block's
9686
+ # window, ascending by effective_reset_at_utc, so the text
9687
+ # renderer can interleave a ``⚡ CREDIT -Xpp @ HH:MM`` divider
9688
+ # row between pre- and post-credit milestone segments and JSON
9689
+ # consumers see the parallel ``credits[]`` array (Section 5.2).
9690
+ credit_rows = conn.execute(
9691
+ """
9692
+ SELECT effective_reset_at_utc, prior_percent, post_percent
9693
+ FROM five_hour_reset_events
9694
+ WHERE five_hour_window_key = ?
9695
+ ORDER BY effective_reset_at_utc ASC
9696
+ """,
9697
+ (block["five_hour_window_key"],),
9698
+ ).fetchall()
9699
+ credits_list: list[dict] = [
9700
+ {
9701
+ "effectiveResetAtUtc": c["effective_reset_at_utc"],
9702
+ "priorPercent": float(c["prior_percent"]),
9703
+ "postPercent": float(c["post_percent"]),
9704
+ "deltaPp": round(
9705
+ float(c["post_percent"]) - float(c["prior_percent"]), 1
9706
+ ),
9707
+ }
9708
+ for c in credit_rows
9709
+ ]
9710
+
9593
9711
  crossed = bool(block.get("crossed_seven_day_reset"))
9594
9712
  p_start = block.get("seven_day_pct_at_block_start")
9595
9713
  p_end = block.get("seven_day_pct_at_block_end")
@@ -9626,6 +9744,10 @@ def cmd_five_hour_breakdown(args: argparse.Namespace) -> int:
9626
9744
  "sevenDayPctDeltaPp": delta,
9627
9745
  "crossedSevenDayReset": crossed,
9628
9746
  }
9747
+ # Spec §5.2: expose ``resetEventId`` on each milestone so JSON
9748
+ # consumers can disambiguate post-credit threshold repeats from
9749
+ # pre-credit ones. ``0`` is the pre-credit/no-credit sentinel
9750
+ # (matches the schema default).
9629
9751
  ms_out = [
9630
9752
  {
9631
9753
  "percentThreshold": m["percent_threshold"],
@@ -9636,13 +9758,23 @@ def cmd_five_hour_breakdown(args: argparse.Namespace) -> int:
9636
9758
  else round(m["marginal_cost_usd"], 9)
9637
9759
  ),
9638
9760
  "sevenDayPctAtCrossing": m["seven_day_pct_at_crossing"],
9761
+ "resetEventId": int(m["reset_event_id"] or 0),
9639
9762
  }
9640
9763
  for m in milestones
9641
9764
  ]
9642
9765
 
9643
9766
  if args.json:
9767
+ # Spec §5.2: ``credits`` is the parallel array to
9768
+ # ``milestones`` — same shape as the ``credits`` field on
9769
+ # ``five-hour-blocks --json`` (§5.1). Stacked credits across
9770
+ # distinct 10-min slots produce multiple entries.
9644
9771
  print(json.dumps(
9645
- {"schemaVersion": 1, "block": block_out, "milestones": ms_out},
9772
+ {
9773
+ "schemaVersion": 1,
9774
+ "block": block_out,
9775
+ "milestones": ms_out,
9776
+ "credits": credits_list,
9777
+ },
9646
9778
  indent=2,
9647
9779
  ))
9648
9780
  return 0
@@ -9683,7 +9815,47 @@ def cmd_five_hour_breakdown(args: argparse.Namespace) -> int:
9683
9815
  headers = ["#", "Threshold", "Cumulative Cost", "Marginal Cost",
9684
9816
  "7d at crossing"]
9685
9817
  rows = []
9686
- for idx, m in enumerate(ms_out, start=1):
9818
+ # Spec §5.2 merged event stream. Interleave milestones and
9819
+ # credits in time-order (``capturedAt`` for milestones,
9820
+ # ``effectiveResetAtUtc`` for credits). Credits render as a
9821
+ # divider row with ``⚡ CREDIT`` in the Threshold cell and the
9822
+ # delta-pp + HH:MM in the rightmost cell; the milestone row
9823
+ # numbering counter (``#``) continues across the divider so the
9824
+ # ordinal still reflects "the Nth event in this block."
9825
+ merged_events: list[tuple[str, dict]] = []
9826
+ for m in ms_out:
9827
+ merged_events.append(("milestone", m))
9828
+ for c in credits_list:
9829
+ merged_events.append(("credit", c))
9830
+ merged_events.sort(key=lambda ev: (
9831
+ ev[1]["effectiveResetAtUtc"] if ev[0] == "credit"
9832
+ else ev[1]["capturedAt"]
9833
+ ))
9834
+ idx = 0
9835
+ for kind, ev in merged_events:
9836
+ idx += 1
9837
+ if kind == "credit":
9838
+ # Spec §5.2: ⚡ CREDIT -Xpp @ HH:MM divider row.
9839
+ # HH:MM rendered in the display tz via format_display_dt.
9840
+ # ``format_display_dt`` is the documented chokepoint for
9841
+ # human-displayed datetimes (CLAUDE.md). The deltaPp
9842
+ # value is float; format as integer ppm (mirrors the
9843
+ # five-hour-blocks chip in §5.1).
9844
+ hhmm = format_display_dt(
9845
+ ev["effectiveResetAtUtc"],
9846
+ args._resolved_tz,
9847
+ fmt="%H:%M",
9848
+ suffix=False,
9849
+ )
9850
+ rows.append([
9851
+ str(idx),
9852
+ "⚡ CREDIT",
9853
+ f"{ev['deltaPp']:+.0f}pp",
9854
+ "",
9855
+ f"@ {hhmm}",
9856
+ ])
9857
+ continue
9858
+ m = ev
9687
9859
  cum = f"${m['blockCostUSD']:.6f}"
9688
9860
  marg = (
9689
9861
  "n/a" if m["marginalCostUSD"] is None
@@ -13832,8 +14004,21 @@ def _build_five_hour_blocks_snapshot(
13832
14004
  used_pct = float(r.get("final_five_hour_percent") or 0.0)
13833
14005
  crossed = bool(r.get("crossed_seven_day_reset"))
13834
14006
  cell_text = "⚡" if crossed else "—"
14007
+ # Spec §5.1.1 (Codex r2 finding 3): consume the ``__credits``
14008
+ # side-channel set by ``cmd_five_hour_blocks`` and append a
14009
+ # ``⚡ -Xpp, -Ypp`` chip to the block_start cell. Pure-string
14010
+ # cell content flows uniformly through markdown / HTML table /
14011
+ # SVG text renderers without per-format additions. Symmetric to
14012
+ # the existing ⚡ glyph in the cross_reset cell — by position
14013
+ # (block_start suffix vs. dedicated column) the two annotations
14014
+ # remain visually distinguishable.
14015
+ credits = r.get("__credits") or []
14016
+ block_cell = block_lbl
14017
+ if credits:
14018
+ deltas = ", ".join(f"{c['deltaPp']:+.0f}pp" for c in credits)
14019
+ block_cell = f"{block_lbl} ⚡ {deltas}"
13835
14020
  snap_rows.append(_lib_share.Row(cells={
13836
- "block_start": _lib_share.TextCell(block_lbl),
14021
+ "block_start": _lib_share.TextCell(block_cell),
13837
14022
  "cost": _lib_share.MoneyCell(cost_usd),
13838
14023
  "used_pct": _lib_share.PercentCell(used_pct),
13839
14024
  "cross_reset": _lib_share.TextCell(cell_text),