cctally 1.109.0 → 1.111.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 (47) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +6 -6
  3. package/bin/_cctally_cache.py +62 -13
  4. package/bin/_cctally_core.py +8 -1
  5. package/bin/_cctally_dashboard.py +422 -195
  6. package/bin/_cctally_dashboard_conversation.py +231 -40
  7. package/bin/_cctally_dashboard_envelope.py +48 -32
  8. package/bin/_cctally_dashboard_sources.py +503 -109
  9. package/bin/_cctally_db.py +44 -0
  10. package/bin/_cctally_diagnosis_sources.py +181 -15
  11. package/bin/_cctally_doctor.py +15 -3
  12. package/bin/_cctally_forecast.py +25 -12
  13. package/bin/_cctally_journal.py +59 -32
  14. package/bin/_cctally_milestone_history.py +65 -9
  15. package/bin/_cctally_parser.py +5 -0
  16. package/bin/_cctally_record.py +50 -12
  17. package/bin/_cctally_rederive.py +899 -30
  18. package/bin/_cctally_source_analytics.py +567 -133
  19. package/bin/_cctally_transcript.py +27 -2
  20. package/bin/_cctally_tui.py +39 -6
  21. package/bin/_cctally_update.py +420 -14
  22. package/bin/_lib_cache_report.py +7 -10
  23. package/bin/_lib_codex_conversation_query.py +286 -63
  24. package/bin/_lib_codex_metadata.py +207 -0
  25. package/bin/_lib_conversation_query.py +254 -37
  26. package/bin/_lib_doctor.py +16 -2
  27. package/bin/_lib_pricing.py +80 -276
  28. package/bin/_lib_rederive.py +903 -2
  29. package/bin/_lib_view_models.py +5 -0
  30. package/bin/cctally +18 -5
  31. package/dashboard/static/assets/ConversationsView-BB9vUiL6.js +72 -0
  32. package/dashboard/static/assets/{DoctorModal-BZGXOlc2.js → DoctorModal-DFY2iPDf.js} +1 -1
  33. package/dashboard/static/assets/ModalRoot-DvRKHbQE.js +1 -0
  34. package/dashboard/static/assets/{ProjectsDrillPanel-KbO_KWa7.js → ProjectsDrillPanel-DAv39uSo.js} +1 -1
  35. package/dashboard/static/assets/{SourceDetailModal-BrOPLVRL.js → SourceDetailModal-DSAWo2Fp.js} +1 -1
  36. package/dashboard/static/assets/{UpdateModal-CYgEFAi4.js → UpdateModal-R_9dOVnh.js} +3 -3
  37. package/dashboard/static/assets/index-COhxe3vU.css +1 -0
  38. package/dashboard/static/assets/index-DdU62sov.js +13 -0
  39. package/dashboard/static/assets/outlineNavigation-BOCLcRa6.js +9 -0
  40. package/dashboard/static/assets/{useKeymap-DQx_A1MB.js → useKeymap-CW7Up0Ay.js} +1 -1
  41. package/dashboard/static/dashboard.html +3 -3
  42. package/package.json +2 -1
  43. package/dashboard/static/assets/ConversationsView-TO5bpIyc.js +0 -72
  44. package/dashboard/static/assets/ModalRoot-C_fvugTl.js +0 -1
  45. package/dashboard/static/assets/index-Cj_l7T7u.css +0 -1
  46. package/dashboard/static/assets/index-DLVwACUM.js +0 -13
  47. package/dashboard/static/assets/outlineNavigation-DXX-RjKl.js +0 -9
@@ -320,7 +320,42 @@ def _claude_cycle_rows(conn: sqlite3.Connection, ref) -> list:
320
320
  ).fetchall()
321
321
 
322
322
 
323
- def _index_entry(conn, ref, current_cycle_key, tz) -> dict:
323
+ def _five_hour_detail_revision(conn, start_iso, end_iso) -> tuple[int, ...]:
324
+ """Append-only 5h child revision for one cycle's intersecting blocks.
325
+
326
+ The detail cache uses the index stamp. Weekly crossing counts do not move
327
+ when another account records a 5h credit or crossing in an existing block.
328
+ These child tables insert new ids for those events; count and max id also
329
+ notice deletions and replacement rows without hashing an unbounded ledger.
330
+ """
331
+ if not start_iso or not end_iso:
332
+ return (0, 0, 0, 0, 0, 0)
333
+ interval = (
334
+ "unixepoch(b.block_start_at) < unixepoch(?) "
335
+ "AND unixepoch(b.five_hour_resets_at) > unixepoch(?)"
336
+ )
337
+ bounds = (end_iso, start_iso)
338
+ blocks = conn.execute(
339
+ "SELECT COUNT(*), MAX(b.id) FROM five_hour_blocks b "
340
+ f"WHERE {interval}", bounds,
341
+ ).fetchone()
342
+ milestones = conn.execute(
343
+ "SELECT COUNT(*), MAX(m.id) FROM five_hour_blocks b "
344
+ "JOIN five_hour_milestones m ON m.block_id = b.id "
345
+ f"WHERE {interval}", bounds,
346
+ ).fetchone()
347
+ credits = conn.execute(
348
+ "SELECT COUNT(*), MAX(e.id) FROM five_hour_blocks b "
349
+ "JOIN five_hour_reset_events e "
350
+ "ON e.account_key = b.account_key "
351
+ "AND e.five_hour_window_key = b.five_hour_window_key "
352
+ f"WHERE {interval}", bounds,
353
+ ).fetchone()
354
+ return tuple(int(value or 0) for row in (blocks, milestones, credits)
355
+ for value in row)
356
+
357
+
358
+ def _index_entry(conn, ref, current_cycle_key, tz, *, decorated=False) -> dict:
324
359
  rows = _claude_cycle_rows(conn, ref)
325
360
  milestone_count = len(rows)
326
361
  segment_count = 1 if rows else 0
@@ -329,6 +364,11 @@ def _index_entry(conn, ref, current_cycle_key, tz) -> dict:
329
364
  end_z = _to_iso_z(ref.week_end_at)
330
365
  block_count = _count_blocks(conn, start_z, end_z)
331
366
  key = _claude_cycle_key(ref)
367
+ # R8: leave the <=1-real-account stamp byte-identical. On a decorated
368
+ # provider the fetched block list contains multiple account-owned rows,
369
+ # so its cache identity also needs their child-ledger revision.
370
+ block_revision = (
371
+ _five_hour_detail_revision(conn, start_z, end_z) if decorated else ())
332
372
 
333
373
  # #750 S4 §5.3. A CURRENT single-segment cycle neither fetches its detail
334
374
  # nor uses one that arrives, so a gap in the LIVE cycle would stay
@@ -356,7 +396,8 @@ def _index_entry(conn, ref, current_cycle_key, tz) -> dict:
356
396
  "segment_count": segment_count,
357
397
  "has_observation_gap": has_gap,
358
398
  "detail_stamp": _mh.compute_detail_stamp(
359
- key, milestone_count, block_count, segment_count, max_captured
399
+ key, milestone_count, block_count, segment_count, max_captured,
400
+ *block_revision,
360
401
  ),
361
402
  }
362
403
 
@@ -377,7 +418,12 @@ def build_claude_week_index(conn: sqlite3.Connection) -> list:
377
418
  )
378
419
  current_cycle_key = _claude_cycle_key(current_ref) if current_ref else None
379
420
  tz = _resolve_display_tz()
380
- entries = [_index_entry(conn, ref, current_cycle_key, tz) for ref in refs]
421
+ import _cctally_account
422
+ decorated = _cctally_account.provider_is_decorated(conn, "claude")
423
+ entries = [
424
+ _index_entry(conn, ref, current_cycle_key, tz, decorated=decorated)
425
+ for ref in refs
426
+ ]
381
427
  entries.sort(key=lambda e: e["start_at_utc"] or "", reverse=True)
382
428
  return entries
383
429
 
@@ -443,14 +489,17 @@ def _shape_observation_gap_runs(disclosure, rows) -> list:
443
489
  return out
444
490
 
445
491
 
446
- def _load_block_credits(conn: sqlite3.Connection, window_key: int) -> list:
492
+ def _load_block_credits(
493
+ conn: sqlite3.Connection, window_key: int, account_key: str,
494
+ ) -> list:
447
495
  """5h in-place credit rows for a block, ascending by effective time —
448
496
  same wire shape as the envelope's ``five_hour_block.credits``."""
449
497
  rows = conn.execute(
450
498
  "SELECT effective_reset_at_utc, prior_percent, post_percent "
451
- "FROM five_hour_reset_events WHERE five_hour_window_key = ? "
499
+ "FROM five_hour_reset_events "
500
+ "WHERE five_hour_window_key = ? AND account_key = ? "
452
501
  "ORDER BY effective_reset_at_utc ASC",
453
- (int(window_key),),
502
+ (int(window_key), account_key),
454
503
  ).fetchall()
455
504
  return [
456
505
  {
@@ -470,6 +519,8 @@ def _build_blocks(conn: sqlite3.Connection, start_iso, end_iso) -> list:
470
519
  if not start_iso or not end_iso:
471
520
  return []
472
521
  c = _cctally()
522
+ import _cctally_account
523
+ decorated = _cctally_account.provider_is_decorated(conn, "claude")
473
524
  # #834 S1 (#836): `id` is selected so each block's milestones are loaded by
474
525
  # the PRECISE selector. This route used to select blocks account-blind and
475
526
  # then pass a bare `five_hour_window_key` to the milestone read, while block
@@ -477,13 +528,13 @@ def _build_blocks(conn: sqlite3.Connection, start_iso, end_iso) -> list:
477
528
  # window's per-account blocks each rendered EVERY account's milestones. `id`
478
529
  # stays internal: it is not added to the published `blocks[]` shape.
479
530
  rows = conn.execute(
480
- "SELECT id, five_hour_window_key, block_start_at, five_hour_resets_at, "
531
+ "SELECT id, account_key, five_hour_window_key, block_start_at, five_hour_resets_at, "
481
532
  " final_five_hour_percent, total_cost_usd, "
482
533
  " crossed_seven_day_reset, is_closed "
483
534
  "FROM five_hour_blocks "
484
535
  "WHERE unixepoch(block_start_at) < unixepoch(?) "
485
536
  " AND unixepoch(five_hour_resets_at) > unixepoch(?) "
486
- "ORDER BY unixepoch(block_start_at) ASC, five_hour_window_key ASC",
537
+ "ORDER BY unixepoch(block_start_at) ASC, five_hour_window_key ASC, id ASC",
487
538
  (end_iso, start_iso),
488
539
  ).fetchall()
489
540
  out: list = []
@@ -502,7 +553,12 @@ def _build_blocks(conn: sqlite3.Connection, start_iso, end_iso) -> list:
502
553
  "is_closed": bool(b["is_closed"]),
503
554
  "milestones": c._tui_build_five_hour_milestones(
504
555
  conn, wk, block_id=int(b["id"])),
505
- "credits": _load_block_credits(conn, wk),
556
+ "credits": _load_block_credits(conn, wk, b["account_key"]),
557
+ **({
558
+ "account_key": b["account_key"],
559
+ "account_label": _cctally_account.display_account_label(
560
+ conn, b["account_key"]),
561
+ } if decorated else {}),
506
562
  }
507
563
  )
508
564
  return out
@@ -3175,6 +3175,11 @@ def _build_db_parser(subparsers, name, *, help_text, xref=None):
3175
3175
  action="store_true",
3176
3176
  help="Append the correction batch and rebuild stats.db (default: preview)",
3177
3177
  )
3178
+ db_rederive.add_argument(
3179
+ "--reviewed-weekly-decisions",
3180
+ metavar="PATH",
3181
+ help="Review exact Claude weekly observations from a pinned manifest",
3182
+ )
3178
3183
  db_rederive.add_argument(
3179
3184
  "--json",
3180
3185
  action="store_true",
@@ -3922,7 +3922,7 @@ def detect_reset_and_credit(conn, *, week_start_date, week_end_at,
3922
3922
  five_hour_percent, as_of=None, commit=True,
3923
3923
  ctx=None, account_key=_lib_accounts.UNATTRIBUTED,
3924
3924
  origin_observation_id=None, capture_at=None,
3925
- source=None):
3925
+ source=None, hold_weekly_axis=False):
3926
3926
  """Detect + record weekly and 5h reset/credit artifacts for one usage
3927
3927
  observation (extracted from ``cmd_record_usage``; DB journal redesign
3928
3928
  §5.2.3).
@@ -3986,6 +3986,10 @@ def detect_reset_and_credit(conn, *, week_start_date, week_end_at,
3986
3986
  one value. The weekly branch does not read it: extending source-local
3987
3987
  confirmation to the weekly path would delay the immediate 63-to-0 reset
3988
3988
  that #755 exists to stop delaying.
3989
+ - ``hold_weekly_axis`` (default ``False``): scratch replay's explicit
3990
+ reviewed observation hold. It bypasses weekly reset/credit detection
3991
+ while still running the independent five-hour detector below. Live
3992
+ callers retain the default.
3989
3993
  """
3990
3994
  c = _cctally()
3991
3995
  now_utc = _as_of_or_command(as_of)
@@ -4025,7 +4029,7 @@ def detect_reset_and_credit(conn, *, week_start_date, week_end_at,
4025
4029
  "ORDER BY captured_at_utc DESC, id DESC LIMIT 1",
4026
4030
  (account_key,),
4027
4031
  ).fetchone()
4028
- if prior and prior["week_end_at"] and cur_end_canon:
4032
+ if not hold_weekly_axis and prior and prior["week_end_at"] and cur_end_canon:
4029
4033
  prior_end_canon = _canonicalize_optional_iso(
4030
4034
  prior["week_end_at"], "record.prior"
4031
4035
  )
@@ -7265,6 +7269,13 @@ def _pipeline_claude_usage(ctx, rec):
7265
7269
  if "resets_at" not in payload or "weekly_percent" not in payload:
7266
7270
  return
7267
7271
 
7272
+ # The exact raw observation id is reviewed outside this pipeline. A hold
7273
+ # can only be supplied on scratch replay; live ingest has no event sink.
7274
+ hold_weekly_axis = (
7275
+ ctx.event_sink is not None
7276
+ and rec.get("id") in ctx.held_weekly_observation_ids
7277
+ )
7278
+
7268
7279
  c = _cctally()
7269
7280
  conn = ctx.conn
7270
7281
  # `as_of` is the record's `at` (= `_command_as_of()` at capture, CCTALLY_AS_OF
@@ -7326,6 +7337,20 @@ def _pipeline_claude_usage(ctx, rec):
7326
7337
  except (ValueError, TypeError) as exc:
7327
7338
  eprint(f"[ingest] 5h window-key derivation failed: {exc}")
7328
7339
 
7340
+ import _cctally_journal as jr
7341
+ reviewed_weekly_basis = None
7342
+ if hold_weekly_axis:
7343
+ reviewed_weekly_basis = (
7344
+ ctx.reviewed_weekly_basis_by_account.get(account_key)
7345
+ or jr._latest_weekly_basis(conn, account_key)
7346
+ )
7347
+ if reviewed_weekly_basis is None:
7348
+ import _lib_rederive
7349
+ raise _lib_rederive.RederiveConflict(
7350
+ "reviewed weekly hold has no accepted account basis: "
7351
+ f"{rec['id']}"
7352
+ )
7353
+
7329
7354
  # 1. Reset/credit detection (fold into the cycle txn; Design B suppression).
7330
7355
  c.detect_reset_and_credit(
7331
7356
  conn,
@@ -7350,11 +7375,11 @@ def _pipeline_claude_usage(ctx, rec):
7350
7375
  # #769 S2 §3: the contributor discriminator for five-hour credit
7351
7376
  # confirmation. `payload.source`, never `rec["src"]`.
7352
7377
  source=source,
7378
+ hold_weekly_axis=hold_weekly_axis,
7353
7379
  )
7354
7380
 
7355
7381
  # 2. Accept/skip DECISION (clamp + dedup), made ONCE and journaled via the
7356
7382
  # snapshot_accept evt (so replay never re-derives it — spec §5.3).
7357
- import _cctally_journal as jr
7358
7383
  fold = jr._usage_snapshot_fold_decision(conn, {
7359
7384
  "week_start_date": week_start_date,
7360
7385
  "week_start_at": week_start_at,
@@ -7363,10 +7388,11 @@ def _pipeline_claude_usage(ctx, rec):
7363
7388
  "five_hour_percent": five_hour_percent,
7364
7389
  "five_hour_window_key": five_hour_window_key,
7365
7390
  "account_key": account_key,
7366
- })
7367
- # #769 S11 (#824). `weekly_held` says the incoming weekly reading
7368
- # CONTRADICTS the stored one and the weekly axis is held at the stored
7369
- # maximum; `wrote_row` says whether this tick materialized a snapshot at
7391
+ }, force_weekly_hold=hold_weekly_axis,
7392
+ reviewed_weekly_basis=reviewed_weekly_basis)
7393
+ # #769 S11 (#824). `weekly_held` says the weekly axis is held at the
7394
+ # accepted basis (an ordinary lower clamp or an explicit scratch hold);
7395
+ # `wrote_row` says whether this tick materialized a snapshot at
7370
7396
  # all. They are independent now: a held tick DOES write a row when its
7371
7397
  # five-hour axis carries evidence, and that row is the only place the
7372
7398
  # evidence survives, because `db rebuild` reconstructs open blocks from
@@ -7453,12 +7479,25 @@ def _pipeline_claude_usage(ctx, rec):
7453
7479
  if rowid is None:
7454
7480
  return
7455
7481
  saved["id"] = rowid
7482
+ if ctx.event_sink is not None and not weekly_held:
7483
+ # Keep the most recent accepted weekly source across scratch
7484
+ # records. A later five-hour credit may suppress its snapshot
7485
+ # row, but that independent effect must not erase the reviewed
7486
+ # weekly basis for another held observation.
7487
+ basis = jr._latest_weekly_basis(conn, account_key)
7488
+ if basis is not None:
7489
+ ctx.reviewed_weekly_basis_by_account[account_key] = basis
7456
7490
  else:
7491
+ saved_week_start_date = (
7492
+ fold.weekly.basis.week_start_date
7493
+ if weekly_held and fold.weekly.basis is not None
7494
+ else week_start_date
7495
+ )
7457
7496
  latest = conn.execute(
7458
7497
  "SELECT * FROM weekly_usage_snapshots WHERE week_start_date = ? "
7459
7498
  " AND account_key = ? "
7460
7499
  "ORDER BY captured_at_utc DESC, id DESC LIMIT 1",
7461
- (week_start_date, account_key),
7500
+ (saved_week_start_date, account_key),
7462
7501
  ).fetchone()
7463
7502
  if latest is None:
7464
7503
  return # nothing recorded yet -> nothing to derive against
@@ -7472,10 +7511,9 @@ def _pipeline_claude_usage(ctx, rec):
7472
7511
  # crosses a $ threshold still derives it.
7473
7512
  # The one exception is a HELD weekly axis. A no-change tick AGREES with
7474
7513
  # the stored row, so re-deriving against it is the self-heal; a held
7475
- # weekly axis means the incoming 7d percent is strictly BELOW the
7476
- # reset-aware in-window maximum,
7477
- # so the observation CONTRADICTS `saved` and a weekly milestone derived
7478
- # from `saved`'s higher percent records a crossing the meter says did not
7514
+ # weekly axis means the observation's 7d value was not accepted,
7515
+ # so it CONTRADICTS `saved` and a weekly milestone derived
7516
+ # from `saved`'s carried percent can record a crossing the meter did not
7479
7517
  # happen. That is how the 2026-09-01 incident fabricated a 13% milestone
7480
7518
  # in a fresh post-credit epoch from a stale pre-credit replica, and
7481
7519
  # because milestones are forward-only within an epoch that row forecloses