cctally 1.114.0 → 1.115.0

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.
Files changed (64) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/bin/_cctally_account.py +32 -8
  3. package/bin/_cctally_cache.py +4394 -279
  4. package/bin/_cctally_completion.py +1287 -0
  5. package/bin/_cctally_config.py +37 -0
  6. package/bin/_cctally_core.py +178 -2
  7. package/bin/_cctally_dashboard.py +2 -2
  8. package/bin/_cctally_dashboard_conversation.py +292 -23
  9. package/bin/_cctally_dashboard_envelope.py +16 -3
  10. package/bin/_cctally_dashboard_sources.py +18 -9
  11. package/bin/_cctally_db.py +316 -5
  12. package/bin/_cctally_doctor.py +436 -27
  13. package/bin/_cctally_five_hour.py +82 -31
  14. package/bin/_cctally_forecast.py +81 -9
  15. package/bin/_cctally_journal.py +3884 -112
  16. package/bin/_cctally_journal_repair.py +101 -12
  17. package/bin/_cctally_parser.py +42 -0
  18. package/bin/_cctally_project.py +8 -2
  19. package/bin/_cctally_quarantine.py +652 -0
  20. package/bin/_cctally_quota.py +26 -4
  21. package/bin/_cctally_quota_model.py +360 -83
  22. package/bin/_cctally_record.py +677 -149
  23. package/bin/_cctally_rederive.py +297 -32
  24. package/bin/_cctally_refresh.py +84 -13
  25. package/bin/_cctally_source_analytics.py +23 -2
  26. package/bin/_cctally_statusline.py +6137 -517
  27. package/bin/_cctally_store.py +287 -48
  28. package/bin/_cctally_transcript.py +8 -7
  29. package/bin/_cctally_tui.py +57 -53
  30. package/bin/_lib_account_binding.py +152 -0
  31. package/bin/_lib_account_protocol.py +386 -0
  32. package/bin/_lib_aggregators.py +4 -0
  33. package/bin/_lib_cache_coverage.py +13 -1
  34. package/bin/_lib_cache_writer_lock.py +19 -3
  35. package/bin/_lib_clone_copy.py +142 -0
  36. package/bin/_lib_codex_conversation_query.py +133 -19
  37. package/bin/_lib_codex_conversation_watch.py +59 -14
  38. package/bin/_lib_codex_drain_admission.py +214 -0
  39. package/bin/_lib_codex_metadata.py +18 -1
  40. package/bin/_lib_codex_publication_barrier.py +277 -0
  41. package/bin/_lib_codex_rollout_container.py +502 -0
  42. package/bin/_lib_conversation_dispatch.py +3 -0
  43. package/bin/_lib_conversation_query.py +11 -3
  44. package/bin/_lib_detector_access_registry.py +3817 -0
  45. package/bin/_lib_doctor.py +452 -1
  46. package/bin/_lib_five_hour.py +126 -0
  47. package/bin/_lib_ingest_frontier.py +414 -20
  48. package/bin/_lib_journal.py +141 -10
  49. package/bin/_lib_jsonl.py +5 -4
  50. package/bin/_lib_legacy_spool.py +126 -0
  51. package/bin/_lib_legacy_writer_evidence.py +136 -0
  52. package/bin/_lib_merge_gate.py +29 -302
  53. package/bin/_lib_meter_rate_change.py +10 -0
  54. package/bin/_lib_outcome_quarantine.py +690 -0
  55. package/bin/_lib_pricing.py +7 -2
  56. package/bin/_lib_quarantine.py +154 -0
  57. package/bin/_lib_quota_calibration.py +53 -8
  58. package/bin/_lib_quota_copy.py +2 -1
  59. package/bin/_lib_quota_model.py +288 -46
  60. package/bin/_lib_rederive.py +94 -5
  61. package/bin/_lib_segment_summary.py +35 -2
  62. package/bin/_lib_statusline_candidates.py +292 -23
  63. package/bin/cctally +23 -1
  64. package/package.json +14 -1
@@ -45,6 +45,7 @@ import sqlite3
45
45
  import sys
46
46
 
47
47
  import _lib_accounts # pure stdlib kernel; UNATTRIBUTED sentinel (#341)
48
+ from _lib_five_hour import latest_five_hour_reading
48
49
  from _cctally_core import _command_as_of, eprint, now_utc_iso, open_db, parse_iso_datetime
49
50
 
50
51
 
@@ -983,9 +984,10 @@ def _latest_seven_day_and_window(
983
984
  overlooked (#769 S11, #824). A `weekly_observation_held` row's five-hour
984
985
  fields are the writing tick's own, so it is the freshest statement of which
985
986
  window is current; excluding it would resolve an older window and report the
986
- live block closed. Rows with no ``five_hour_window_key`` are included here
987
- too, unchanged, because the latest row's key is the answer even when it is
988
- NULL.
987
+ live block closed. It is the key of the newest row that CARRIES a five-hour
988
+ reading (#825/#832 spec 4.7 revision 7, ``latest_five_hour_reading``): a
989
+ retired replica or a weekly-only reading can be the newest row, and its NULL
990
+ key would report the live block closed and leave its weekly end stale.
989
991
 
990
992
  **The weekly value is CREDIT-AWARE**, which is the #835 change. It comes from
991
993
  the most recent row that is not a post-floor stale replica — the same band
@@ -1065,8 +1067,15 @@ def _latest_seven_day_and_window(
1065
1067
  row = cur.fetchone()
1066
1068
  if row is None:
1067
1069
  return None, None
1068
- # Axis 1: the five-hour window key, from the latest row, held-inclusive.
1069
- key = row["five_hour_window_key"]
1070
+ # Axis 1: the five-hour window key, held-inclusive, from the newest
1071
+ # five-hour READING of the newest row's own account (#825/#832 spec
1072
+ # 4.7 revision 7, the one shared selection). The newest row can be a
1073
+ # retired replica or a weekly-only reading, which names no window.
1074
+ # Merged mode scopes the weekly walk to that account too, so an
1075
+ # account-blind selection here would pair another account's window
1076
+ # with this account's weekly value.
1077
+ reading = latest_five_hour_reading(conn, account_key=row["account_key"])
1078
+ key = None if reading is None else reading.window_key
1070
1079
  # Axis 2: the weekly value. The scope, the bands and the floor all come
1071
1080
  # from the NEWEST row and are resolved ONCE, because the walk may not
1072
1081
  # leave that scope — so there is nothing to memoize per candidate.
@@ -2718,6 +2727,7 @@ def cmd_five_hour_breakdown(args: argparse.Namespace) -> int:
2718
2727
 
2719
2728
  def _backfill_five_hour_blocks(
2720
2729
  conn: sqlite3.Connection, *, only_missing: bool = False,
2730
+ journaled_first: "dict | None" = None,
2721
2731
  ) -> int:
2722
2732
  """One-shot historical backfill of five_hour_blocks from existing
2723
2733
  weekly_usage_snapshots data. Idempotent via UNIQUE(five_hour_window_key)
@@ -2740,9 +2750,58 @@ def _backfill_five_hour_blocks(
2740
2750
  re-materializes only the open block (block-only, no milestones — the
2741
2751
  journaled 5h-milestone evts carry their own values and resolve block_id
2742
2752
  against this row), keeping the pass O(open windows) instead of O(all blocks).
2753
+
2754
+ #825/#832 fix round J4 (#954): the rebuild projection reproduces the open
2755
+ block live ingest left, on its accounting columns too.
2756
+
2757
+ * ``journaled_first`` maps ``(account_key, five_hour_window_key)`` to the
2758
+ window's FIRST WRITER — its first effective `snapshot_accept` decision
2759
+ in journal order (`_cctally_journal.
2760
+ journaled_block_first_observations`) — and supplies the block's
2761
+ immutable start values (`first_observed_at_utc`,
2762
+ `five_hour_resets_at`, `block_start_at`,
2763
+ `seven_day_pct_at_block_start`) whenever it is present. Live ingest's
2764
+ upsert never updates those columns: the writer that materializes the
2765
+ row owns them. Two things separate that writer from the earliest
2766
+ surviving snapshot. A five-hour credit's stale-replica removal can
2767
+ delete its snapshot (the band starts at floor10 of the arming tick, so
2768
+ it reaches the genuine pre-credit reading); the snapshot still observed
2769
+ the block, because the removal retires its weekly VALUE, not the
2770
+ observation (J4). And a reading captured earlier can be appended, and
2771
+ so derived, later (fix round J5, 825-832-CD4-V01): its row survives
2772
+ with the earlier capture, but it updated a block another reading had
2773
+ already opened. Neither survivor-derived pick reproduces live. A
2774
+ window without an entry (a payload without an explicit account) falls
2775
+ back to the earliest surviving snapshot.
2776
+ * The projection stamps the computation-time pricing provenance of the
2777
+ totals it computes, through the live upsert's own helper
2778
+ (`_block_pricing_provenance_json`), so a successor's
2779
+ `five_hour_block_close` freezes it exactly as after live ingest. The
2780
+ ordinary (legacy, pre-journal) backfill keeps NULL: its blocks predate
2781
+ provenance, which `_lib_cost_provenance.historical_close_marker` reads
2782
+ as unproven.
2743
2783
  """
2744
2784
  _c = _cctally()
2745
2785
  inserted = 0
2786
+ journaled_first = (journaled_first or {}) if only_missing else {}
2787
+
2788
+ def _first_row(key, acct):
2789
+ """The window's start row: its first writer (J4/J5), else the
2790
+ earliest surviving snapshot."""
2791
+ journaled = journaled_first.get((acct, key))
2792
+ if journaled is not None:
2793
+ return journaled
2794
+ return conn.execute(
2795
+ """
2796
+ SELECT five_hour_resets_at, captured_at_utc, weekly_percent
2797
+ FROM weekly_usage_snapshots
2798
+ WHERE five_hour_window_key = ? AND account_key = ?
2799
+ AND five_hour_percent IS NOT NULL
2800
+ ORDER BY captured_at_utc ASC, id ASC LIMIT 1
2801
+ """,
2802
+ (key, acct),
2803
+ ).fetchone()
2804
+
2746
2805
  try:
2747
2806
  # Iterate distinct windows that have BOTH a canonical key AND a
2748
2807
  # 5h percent. The percent guard is critical: MAX(five_hour_percent)
@@ -2812,18 +2871,9 @@ def _backfill_five_hour_blocks(
2812
2871
  for key, acct in keys:
2813
2872
  if (acct, key) in seen_keys:
2814
2873
  continue
2815
- # Same MIN-captured pick the insert loop makes below, so the
2874
+ # Same start-row pick the insert loop makes below, so the
2816
2875
  # ownership interval and the inserted row cannot disagree.
2817
- anchor_row = conn.execute(
2818
- """
2819
- SELECT five_hour_resets_at
2820
- FROM weekly_usage_snapshots
2821
- WHERE five_hour_window_key = ? AND account_key = ?
2822
- AND five_hour_percent IS NOT NULL
2823
- ORDER BY captured_at_utc ASC, id ASC LIMIT 1
2824
- """,
2825
- (key, acct),
2826
- ).fetchone()
2876
+ anchor_row = _first_row(key, acct)
2827
2877
  if anchor_row is None:
2828
2878
  continue
2829
2879
  try:
@@ -2849,22 +2899,14 @@ def _backfill_five_hour_blocks(
2849
2899
  conn.execute("BEGIN IMMEDIATE")
2850
2900
  try:
2851
2901
  for key, acct in keys:
2852
- # MIN-captured row defines the immutable block boundary
2853
- # values (deterministic — picking "any in-window row"
2854
- # would be nondeterministic under seconds-level Anthropic
2855
- # ISO jitter that the canonical key collapses). Scoped to
2902
+ # The start row (the first writer, else the MIN-captured
2903
+ # survivor) defines the immutable block boundary values
2904
+ # (deterministic — picking "any in-window row" would be
2905
+ # nondeterministic under seconds-level Anthropic ISO jitter
2906
+ # that the canonical key collapses). Scoped to
2856
2907
  # (window_key, account_key) so a shared window's per-account
2857
2908
  # blocks each read their own account's boundary rows (#341).
2858
- min_row = conn.execute(
2859
- """
2860
- SELECT five_hour_resets_at, captured_at_utc, weekly_percent
2861
- FROM weekly_usage_snapshots
2862
- WHERE five_hour_window_key = ? AND account_key = ?
2863
- AND five_hour_percent IS NOT NULL
2864
- ORDER BY captured_at_utc ASC, id ASC LIMIT 1
2865
- """,
2866
- (key, acct),
2867
- ).fetchone()
2909
+ min_row = _first_row(key, acct)
2868
2910
  max_row = conn.execute(
2869
2911
  """
2870
2912
  SELECT captured_at_utc, weekly_percent, five_hour_percent
@@ -2941,9 +2983,15 @@ def _backfill_five_hour_blocks(
2941
2983
  # JSONL if cache.db is unreadable.
2942
2984
  # skip_sync=True: backfill runs inside open_db(); a sync_cache
2943
2985
  # cascade here would reopen cache.db recursively.
2986
+ pricing_evidence = {} if only_missing else None
2944
2987
  totals = _c._compute_block_totals(
2945
2988
  block_start_dt, last_obs_dt, skip_sync=True,
2946
2989
  owner_key=key, windows=ownership.get(acct, []),
2990
+ pricing_evidence=pricing_evidence,
2991
+ )
2992
+ pricing_provenance_json = (
2993
+ _c._block_pricing_provenance_json(pricing_evidence)
2994
+ if pricing_evidence is not None else None
2947
2995
  )
2948
2996
 
2949
2997
  cur = conn.execute(
@@ -2963,12 +3011,14 @@ def _backfill_five_hour_blocks(
2963
3011
  total_cache_create_tokens,
2964
3012
  total_cache_read_tokens,
2965
3013
  total_cost_usd,
3014
+ pricing_provenance_json,
2966
3015
  is_closed,
2967
3016
  created_at_utc,
2968
3017
  last_updated_at_utc,
2969
3018
  account_key
2970
3019
  )
2971
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
3020
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
3021
+ ?)
2972
3022
  """,
2973
3023
  (
2974
3024
  key,
@@ -2985,6 +3035,7 @@ def _backfill_five_hour_blocks(
2985
3035
  totals["cache_create_tokens"],
2986
3036
  totals["cache_read_tokens"],
2987
3037
  totals["cost_usd"],
3038
+ pricing_provenance_json,
2988
3039
  is_closed,
2989
3040
  projection_created_at,
2990
3041
  projection_updated_at,
@@ -97,6 +97,7 @@ from _lib_forecast import (
97
97
  # exercises). doctor's _iso_z deliberately keeps its own divergent copy.
98
98
  _ensure_sibling_loaded("_lib_json_envelope")
99
99
  from _lib_json_envelope import _iso_z
100
+ from _lib_five_hour import latest_five_hour_reading
100
101
 
101
102
  # #620 S1 D11: the one affordance shape every warning state renders.
102
103
  _ensure_sibling_loaded("_lib_alert_scope")
@@ -175,7 +176,8 @@ def _week_rows_by_capture(cursor, *, by_id: bool) -> list:
175
176
  def _fetch_current_week_snapshots(conn: sqlite3.Connection, now_utc: dt.datetime,
176
177
  *, account_key: "str | None" = None,
177
178
  include_held: bool = False,
178
- include_account: bool = False):
179
+ include_account: bool = False,
180
+ include_scope: bool = False):
179
181
  """Return (week_start_at, week_end_at, list[(captured_at, percent, five_hr)])
180
182
  for the subscription week containing `now_utc`, or None if no snapshot
181
183
  exists for the current week.
@@ -225,6 +227,14 @@ def _fetch_current_week_snapshots(conn: sqlite3.Connection, now_utc: dt.datetime
225
227
  ``include_account`` appends the physical account key after the held flag
226
228
  for the multi-account Current Week card only. The default wire-independent
227
229
  sample shape and merged forecast/cost reads remain unchanged.
230
+
231
+ ``include_scope`` (#825/#832 spec 4.7 revision 7) appends a fourth element,
232
+ ``(week_start_date, week_start_at_texts)``: the week scope the samples were
233
+ read under, in the form ``latest_five_hour_reading`` takes
234
+ (``week_start_at_texts`` is ``None`` on the date-only fallback). A caller
235
+ reading the five-hour axis selects its reading under exactly this scope
236
+ rather than from the newest sample, which a retired replica or a
237
+ weekly-only reading leaves without a five-hour value.
228
238
  """
229
239
  # #341: optional account scoping — same predicate on every leg so the whole
230
240
  # window resolution stays consistent (a real key or the `unattributed`
@@ -296,6 +306,8 @@ def _fetch_current_week_snapshots(conn: sqlite3.Connection, now_utc: dt.datetime
296
306
  samples = _shape_week_samples(rows, include_held=include_held,
297
307
  include_account=include_account)
298
308
  samples = [s for s in samples if s[0] <= now_utc]
309
+ if include_scope:
310
+ return week_start_at, week_end_at, samples, (drow[0], None)
299
311
  return week_start_at, week_end_at, samples
300
312
  row = chosen
301
313
  week_start_at = parse_iso_datetime(row[0], "week_start_at")
@@ -327,6 +339,9 @@ def _fetch_current_week_snapshots(conn: sqlite3.Connection, now_utc: dt.datetime
327
339
  samples = _shape_week_samples(rows, include_held=include_held,
328
340
  include_account=include_account)
329
341
  samples = [s for s in samples if s[0] <= now_utc]
342
+ if include_scope:
343
+ return (week_start_at, week_end_at, samples,
344
+ (chosen_date, tuple(matching_texts)))
330
345
  return week_start_at, week_end_at, samples
331
346
 
332
347
 
@@ -374,6 +389,42 @@ def _apply_midweek_reset_override(
374
389
  return week_start_at, samples
375
390
 
376
391
 
392
+ def _current_week_five_hour_percent(conn, now_utc, week_scope, *,
393
+ account_key, week_start_at,
394
+ week_start_before_override):
395
+ """The percentage of ``_current_week_five_hour_reading``, or ``None``."""
396
+ reading = _current_week_five_hour_reading(
397
+ conn, now_utc, week_scope, account_key=account_key,
398
+ week_start_at=week_start_at,
399
+ week_start_before_override=week_start_before_override)
400
+ return None if reading is None else reading.percent
401
+
402
+
403
+ def _current_week_five_hour_reading(conn, now_utc, week_scope, *,
404
+ account_key, week_start_at,
405
+ week_start_before_override):
406
+ """The current week's five-hour reading: the newest five-hour READING
407
+ under the current-week read's own scope (#825/#832 spec 4.7 revision 7).
408
+ Its percent and reset come from one row, so a caller that shows both
409
+ shows one reading.
410
+
411
+ ``week_scope`` is ``_fetch_current_week_snapshots(include_scope=True)``'s
412
+ fourth element. When ``_apply_midweek_reset_override`` moved the week's
413
+ start, its new start bounds the captures exactly as it bounded the samples.
414
+ ``account_key`` ``None`` keeps the merged read's account-blind contract.
415
+ """
416
+ week_start_date, week_start_at_texts = week_scope
417
+ floor = (week_start_at if week_start_at != week_start_before_override
418
+ else None)
419
+ reading = latest_five_hour_reading(
420
+ conn, account_key=account_key,
421
+ as_of_utc=_iso_z(now_utc),
422
+ captured_at_or_after_utc=None if floor is None else _iso_z(floor),
423
+ week_start_date=week_start_date,
424
+ week_start_at_in=week_start_at_texts)
425
+ return reading
426
+
427
+
377
428
  def _resolve_current_budget_window(conn, now_utc, *, account_key=None):
378
429
  """Return ``(effective_week_start_dt, week_end_dt)`` for the subscription
379
430
  week containing ``now_utc``, honoring a mid-week reset re-anchor; or
@@ -807,8 +858,13 @@ def _dpp_candidate_comparability(regime, week_start_at, week_end_at, *,
807
858
 
808
859
  - `"comparable"` — in regime, and its own population passes the support
809
860
  test.
810
- - `"incomparable"` — in regime, population read, support test failed.
811
- This is drift, and only this.
861
+ - `"incomparable"` — in regime, population read, and `apply_regime`
862
+ rejected it. That is drift (the support test failed), or a week outside
863
+ the regime's coefficient era (`unvalidated-coefficient-era`, #978 spec
864
+ 4.6), whose units were weighted with another era's coefficients and so
865
+ cannot be compared with this regime's rate either, or a week with no
866
+ local population to weigh (`no-local-history`). All three mean the
867
+ same thing to the selector: the candidate is not comparable.
812
868
  - `"excluded"` — on the far side of a rate boundary.
813
869
  - `"unreadable"` — the population could not be read at all. Folding this
814
870
  into `"excluded"` made a locked `cache.db` look like twelve weeks that
@@ -837,7 +893,8 @@ def _dpp_candidate_comparability(regime, week_start_at, week_end_at, *,
837
893
  cache_conn_factory=cache_conn_factory)
838
894
  except Exception: # noqa: BLE001
839
895
  return "unreadable"
840
- applied = qcg.apply_regime(regime, entries)
896
+ applied = qcg.apply_regime(regime, entries,
897
+ interval=(week_start_at, week_end_at))
841
898
  if isinstance(applied, qcg.ApplyRejection):
842
899
  return "incomparable"
843
900
  return "comparable"
@@ -1289,7 +1346,12 @@ def _calibrated_week_detail(
1289
1346
  week_start_at, horizon, account_key=account_key)
1290
1347
  except sqlite3.Error:
1291
1348
  return CalibratedWeek(code="unavailable")
1292
- applied = qcg.apply_regime(read.regime, entries)
1349
+ # The interval is the whole week the projection extrapolates over, not
1350
+ # the entries' span: a week straddling the regime's coefficient era is
1351
+ # refused as `unvalidated-coefficient-era` even when every retained entry
1352
+ # so far is in-era (#978 spec 4.6).
1353
+ applied = qcg.apply_regime(read.regime, entries,
1354
+ interval=(week_start_at, week_end_at))
1293
1355
  if isinstance(applied, qcg.ApplyRejection):
1294
1356
  return CalibratedWeek(code=applied.value)
1295
1357
 
@@ -1392,12 +1454,14 @@ def _load_forecast_inputs(
1392
1454
  c = _cctally()
1393
1455
  # #769 S11 (#824): ONE read, held-inclusive, split per axis below.
1394
1456
  fetched = _fetch_current_week_snapshots(
1395
- conn, now_utc, account_key=account_key, include_held=True)
1457
+ conn, now_utc, account_key=account_key, include_held=True,
1458
+ include_scope=True)
1396
1459
  if fetched is None:
1397
1460
  return None
1398
- week_start_at, week_end_at, observations = fetched
1461
+ week_start_at, week_end_at, observations, week_scope = fetched
1399
1462
  if not observations:
1400
1463
  return None
1464
+ week_start_before_override = week_start_at
1401
1465
 
1402
1466
  # Mid-week reset override: shift week_start_at to the effective
1403
1467
  # reset moment and drop pre-reset samples so elapsed/remaining math
@@ -1421,8 +1485,16 @@ def _load_forecast_inputs(
1421
1485
  p_now = latest[1]
1422
1486
  # The five-hour axis, which is why the read is held-inclusive: a held row
1423
1487
  # exists precisely because its five-hour reading GREW, so the newest
1424
- # observation of either kind is the current five-hour percentage.
1425
- five_hr = observations[-1][2]
1488
+ # READING of either kind is the current five-hour percentage. It is
1489
+ # selected through the one shared selection (#825/#832 spec 4.7 revision
1490
+ # 7) under this read's own scope: the account, the week, `now_utc`, and
1491
+ # the mid-week reset's lower bound when the override applied. The newest
1492
+ # observation is not the answer, because a retired replica or a
1493
+ # weekly-only reading carries no five-hour value.
1494
+ five_hr = _current_week_five_hour_percent(
1495
+ conn, now_utc, week_scope, account_key=account_key,
1496
+ week_start_at=week_start_at,
1497
+ week_start_before_override=week_start_before_override)
1426
1498
 
1427
1499
  elapsed_hours = (now_utc - week_start_at).total_seconds() / 3600.0
1428
1500
  total_hours = (week_end_at - week_start_at).total_seconds() / 3600.0