cctally 1.7.3 → 1.7.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.
@@ -55,6 +55,23 @@ def _cctally():
55
55
  return sys.modules["cctally"]
56
56
 
57
57
 
58
+ # === Honest imports from extracted homes ===================================
59
+ # Spec 2026-05-17-cctally-core-kernel-extraction.md §3.3: kernel symbols
60
+ # import from _cctally_core. `load_config` plus out-of-scope helpers
61
+ # (`_resolve_oauth_token`, `_fetch_oauth_usage`, `_bust_statusline_cache`,
62
+ # `_refresh_usage_inproc`, `_get_oauth_usage_config`,
63
+ # `_seconds_since_iso`, `_select_last_known_snapshot`,
64
+ # `_newest_snapshot_age_seconds`, `_normalize_percent`,
65
+ # `_forecast_color_enabled`, `_discover_cc_version`, `cmd_record_usage`)
66
+ # stay on the _cctally() accessor.
67
+ from _cctally_core import (
68
+ eprint,
69
+ now_utc_iso,
70
+ _iso_to_epoch,
71
+ _format_short_duration,
72
+ )
73
+
74
+
58
75
  # =========================================================================
59
76
  # Exception classes
60
77
  # =========================================================================
@@ -289,14 +306,13 @@ def _render_refresh_usage_text(payload: dict, color: bool, now_epoch: int) -> st
289
306
  The ``5h`` segment is omitted entirely when ``payload["five_hour"]`` is None.
290
307
  Color codes are emitted only when ``color`` is True.
291
308
  """
292
- c = _cctally()
293
309
  a = _REFRESH_USAGE_ANSI if color else {k: "" for k in _REFRESH_USAGE_ANSI}
294
310
 
295
311
  seven = payload["seven_day"]
296
312
  seven_pct = seven["used_percent"]
297
313
  seven_resets = seven.get("resets_at_epoch")
298
314
  if seven_resets is not None:
299
- seven_ttl = c._format_short_duration(seven_resets - now_epoch)
315
+ seven_ttl = _format_short_duration(seven_resets - now_epoch)
300
316
  seven_seg = (
301
317
  f"{a['yellow']}7d {seven_pct:.0f}%{a['reset']}"
302
318
  f" {a['orange']}(in {seven_ttl}){a['reset']}"
@@ -309,7 +325,7 @@ def _render_refresh_usage_text(payload: dict, color: bool, now_epoch: int) -> st
309
325
  five_pct = five["used_percent"]
310
326
  five_resets = five.get("resets_at_epoch")
311
327
  if five_resets is not None:
312
- five_ttl = c._format_short_duration(five_resets - now_epoch)
328
+ five_ttl = _format_short_duration(five_resets - now_epoch)
313
329
  five_seg = (
314
330
  f" | {a['yellow']}5h {five_pct:.0f}%{a['reset']}"
315
331
  f" {a['orange']}(in {five_ttl}){a['reset']}"
@@ -420,14 +436,13 @@ def _bust_statusline_cache(path: str = _STATUSLINE_OAUTH_CACHE) -> str:
420
436
  ``"absent"`` (file did not exist), ``"error"`` (delete failed for
421
437
  a non-FileNotFoundError reason — logged via eprint, does NOT raise).
422
438
  """
423
- c = _cctally()
424
439
  try:
425
440
  os.remove(path)
426
441
  return "busted"
427
442
  except FileNotFoundError:
428
443
  return "absent"
429
444
  except OSError as exc:
430
- c.eprint(f"refresh-usage: cache-bust failed: {exc}")
445
+ eprint(f"refresh-usage: cache-bust failed: {exc}")
431
446
  return "error"
432
447
 
433
448
 
@@ -450,7 +465,7 @@ def _cmd_refresh_usage_handle_rate_limit(args: argparse.Namespace, exc) -> int:
450
465
  quiet = bool(getattr(args, "quiet", False))
451
466
 
452
467
  if snap is None:
453
- c.eprint("refresh-usage: rate-limited; no last-known data; "
468
+ eprint("refresh-usage: rate-limited; no last-known data; "
454
469
  "status-line will populate on next CC tick")
455
470
  if json_mode:
456
471
  print(json.dumps({
@@ -467,7 +482,7 @@ def _cmd_refresh_usage_handle_rate_limit(args: argparse.Namespace, exc) -> int:
467
482
  _freshness_label(age_s, cfg) if age_s is not None else "stale"
468
483
  )
469
484
 
470
- c.eprint(f"refresh-usage: rate-limited; using last-known "
485
+ eprint(f"refresh-usage: rate-limited; using last-known "
471
486
  f"(captured {int(age_s) if age_s is not None else '?'}s ago)")
472
487
 
473
488
  if json_mode:
@@ -550,7 +565,7 @@ def _refresh_usage_inproc(timeout_seconds: float = 5.0) -> _RefreshUsageResult:
550
565
  # built then a downstream cmd_record_usage call fails).
551
566
  seven_pct = c._normalize_percent(float(seven["utilization"]))
552
567
  seven_resets_iso = seven["resets_at"]
553
- seven_resets_epoch = c._iso_to_epoch(seven_resets_iso)
568
+ seven_resets_epoch = _iso_to_epoch(seven_resets_iso)
554
569
  except (TypeError, ValueError, KeyError) as exc:
555
570
  return _RefreshUsageResult(
556
571
  status="parse_failed",
@@ -566,7 +581,7 @@ def _refresh_usage_inproc(timeout_seconds: float = 5.0) -> _RefreshUsageResult:
566
581
  try:
567
582
  five_pct = c._normalize_percent(float(five["utilization"]))
568
583
  five_resets_iso = five["resets_at"]
569
- five_resets_epoch = c._iso_to_epoch(five_resets_iso)
584
+ five_resets_epoch = _iso_to_epoch(five_resets_iso)
570
585
  except (TypeError, ValueError) as exc:
571
586
  # 5h is optional - silently degrade rather than fail the command
572
587
  # (parity with the previous cmd_refresh_usage behavior; the eprint
@@ -599,7 +614,7 @@ def _refresh_usage_inproc(timeout_seconds: float = 5.0) -> _RefreshUsageResult:
599
614
  # (tests/test_refresh_usage_cmd.py:55, test_refresh_usage_inproc.py:18).
600
615
  cache_state = c._bust_statusline_cache()
601
616
 
602
- fetched_at = c.now_utc_iso()
617
+ fetched_at = now_utc_iso()
603
618
  fresh_envelope = {
604
619
  "label": "fresh",
605
620
  "captured_at": fetched_at,
@@ -652,7 +667,7 @@ def cmd_refresh_usage(args: argparse.Namespace) -> int:
652
667
  # Emit BEFORE rendering so stderr flushes consistently for harnesses
653
668
  # that grep across both streams.
654
669
  for warning in result.warnings:
655
- c.eprint(f"refresh-usage: {warning}")
670
+ eprint(f"refresh-usage: {warning}")
656
671
  payload = result.payload or {}
657
672
  if getattr(args, "json", False):
658
673
  print(_serialize_refresh_usage_json(payload))
@@ -668,7 +683,7 @@ def cmd_refresh_usage(args: argparse.Namespace) -> int:
668
683
  args, RefreshUsageRateLimitError(result.reason or "rate limited"))
669
684
 
670
685
  if result.status == "no_oauth_token":
671
- c.eprint("refresh-usage: no OAuth token found "
686
+ eprint("refresh-usage: no OAuth token found "
672
687
  "(run 'claude' once to authenticate)")
673
688
  return 2
674
689
 
@@ -678,9 +693,9 @@ def cmd_refresh_usage(args: argparse.Namespace) -> int:
678
693
  # config-error reason string verbatim so we can detect it here.
679
694
  reason = result.reason or ""
680
695
  if reason.startswith("invalid oauth_usage config:"):
681
- c.eprint(f"cctally: {reason}")
696
+ eprint(f"cctally: {reason}")
682
697
  return 2
683
- c.eprint(f"refresh-usage: OAuth fetch failed: {reason}")
698
+ eprint(f"refresh-usage: OAuth fetch failed: {reason}")
684
699
  return 3
685
700
 
686
701
  if result.status == "parse_failed":
@@ -688,7 +703,7 @@ def cmd_refresh_usage(args: argparse.Namespace) -> int:
688
703
  # only seven_day failures (RefreshUsageMalformedError or unparseable
689
704
  # field extraction) propagate here. Preserve the original exact
690
705
  # error-line shape for bash harnesses that grep stderr.
691
- c.eprint(f"refresh-usage: {result.reason or ''}")
706
+ eprint(f"refresh-usage: {result.reason or ''}")
692
707
  return 4
693
708
 
694
709
  if result.status == "record_failed":
@@ -698,13 +713,13 @@ def cmd_refresh_usage(args: argparse.Namespace) -> int:
698
713
  rc = int(reason.split()[1])
699
714
  except (IndexError, ValueError):
700
715
  rc = -1
701
- c.eprint(f"refresh-usage: failed to record usage (exit {rc})")
716
+ eprint(f"refresh-usage: failed to record usage (exit {rc})")
702
717
  else:
703
- c.eprint(f"refresh-usage: failed to record usage: {reason}")
718
+ eprint(f"refresh-usage: failed to record usage: {reason}")
704
719
  return 5
705
720
 
706
721
  # Defensive: unknown status from _refresh_usage_inproc -> treat as parse error.
707
- c.eprint(f"refresh-usage: unexpected status {result.status!r}")
722
+ eprint(f"refresh-usage: unexpected status {result.status!r}")
708
723
  return 4
709
724
 
710
725
 
@@ -757,7 +772,7 @@ def _hook_tick_oauth_refresh(
757
772
  seven = api["seven_day"]
758
773
  try:
759
774
  seven_pct = c._normalize_percent(float(seven["utilization"]))
760
- seven_resets_epoch = c._iso_to_epoch(seven["resets_at"])
775
+ seven_resets_epoch = _iso_to_epoch(seven["resets_at"])
761
776
  except (TypeError, ValueError, KeyError):
762
777
  return "err(parse)", None
763
778
  five = api.get("five_hour") if isinstance(api.get("five_hour"), dict) else None
@@ -766,7 +781,7 @@ def _hook_tick_oauth_refresh(
766
781
  if five is not None and "utilization" in five and "resets_at" in five:
767
782
  try:
768
783
  five_pct = c._normalize_percent(float(five["utilization"]))
769
- five_resets_epoch = c._iso_to_epoch(five["resets_at"])
784
+ five_resets_epoch = _iso_to_epoch(five["resets_at"])
770
785
  except (TypeError, ValueError):
771
786
  five_pct = None
772
787
  five_resets_epoch = None
@@ -63,6 +63,18 @@ def _cctally():
63
63
  return sys.modules["cctally"]
64
64
 
65
65
 
66
+ # === Honest imports from extracted homes ===================================
67
+ # Spec 2026-05-17-cctally-core-kernel-extraction.md §3.3: kernel symbols
68
+ # import from _cctally_core. Path constants (`APP_DIR`,
69
+ # `CLAUDE_SETTINGS_PATH`, `HOOK_TICK_LOG_PATH`) plus the extensive
70
+ # out-of-scope setup-specific helpers (legacy migration, hook surgery,
71
+ # OAuth token, sync_cache, …) stay on the _cctally() accessor.
72
+ from _cctally_core import (
73
+ eprint,
74
+ _command_as_of,
75
+ )
76
+
77
+
66
78
  # ── settings.json hook surgery ─────────────────────────────────────────
67
79
 
68
80
 
@@ -430,7 +442,7 @@ def _legacy_resolve_backup_dir() -> pathlib.Path:
430
442
  so a re-run never overwrites a prior migration's snapshot.
431
443
  """
432
444
  c = _cctally()
433
- now = c._command_as_of()
445
+ now = _command_as_of()
434
446
  stamp = now.strftime("%Y%m%d-%H%M%S")
435
447
  base = pathlib.Path.home() / ".claude" / f"{c._LEGACY_BACKUP_DIR_PREFIX}{stamp}"
436
448
  base.mkdir(parents=True, exist_ok=True)
@@ -614,7 +626,6 @@ def _setup_read_legacy_prompt_input(stream, reprompt: str | None = None) -> bool
614
626
  us). When None (test default), no reprompt is emitted — useful for unit
615
627
  tests that drive `stream` from io.StringIO.
616
628
  """
617
- eprint = _cctally().eprint
618
629
  yes_words = {"y", "yes"}
619
630
  no_words = {"n", "no"}
620
631
  for attempt in range(3):
@@ -815,7 +826,7 @@ def _setup_status(args: argparse.Namespace) -> int:
815
826
  try:
816
827
  settings = c._load_claude_settings()
817
828
  except c.SetupError as exc:
818
- c.eprint(f"setup: warning: {exc}")
829
+ eprint(f"setup: warning: {exc}")
819
830
  settings = {}
820
831
  hook_counts = _setup_count_hook_entries(settings)
821
832
  oauth = _setup_oauth_token_present()
@@ -911,14 +922,14 @@ def _setup_uninstall(args: argparse.Namespace) -> int:
911
922
  try:
912
923
  settings = c._load_claude_settings()
913
924
  except c.SetupError as exc:
914
- c.eprint(f"setup: {exc}")
925
+ eprint(f"setup: {exc}")
915
926
  return 1
916
927
  settings, removed = _settings_merge_uninstall(settings)
917
928
  if removed:
918
929
  try:
919
930
  c._write_claude_settings_atomic(settings)
920
931
  except OSError as exc:
921
- c.eprint(f"setup: failed to write {c.CLAUDE_SETTINGS_PATH}: {exc}")
932
+ eprint(f"setup: failed to write {c.CLAUDE_SETTINGS_PATH}: {exc}")
922
933
  return 2
923
934
  out.append(f"Removed {removed} hook entries from {c.CLAUDE_SETTINGS_PATH}")
924
935
 
@@ -938,7 +949,7 @@ def _setup_uninstall(args: argparse.Namespace) -> int:
938
949
  dst.unlink()
939
950
  sym_removed += 1
940
951
  except OSError as exc:
941
- c.eprint(f"setup: failed to remove {dst}: {exc}")
952
+ eprint(f"setup: failed to remove {dst}: {exc}")
942
953
  out.append(f"Removed {sym_removed} symlinks from {dst_dir}/")
943
954
 
944
955
  legacy = _setup_detect_legacy_snippet()
@@ -1000,7 +1011,7 @@ def _setup_uninstall(args: argparse.Namespace) -> int:
1000
1011
  "exit_code": 1,
1001
1012
  }, indent=2))
1002
1013
  else:
1003
- c.eprint(f"setup: failed to wipe {c.APP_DIR}: {exc}")
1014
+ eprint(f"setup: failed to wipe {c.APP_DIR}: {exc}")
1004
1015
  return 1
1005
1016
  else:
1006
1017
  out.append(
@@ -1039,7 +1050,7 @@ def _setup_dry_run(args: argparse.Namespace) -> int:
1039
1050
  # against an empty dict simply yields detected=False for entries (files
1040
1051
  # detection is independent of settings). Mirror _setup_status's pattern
1041
1052
  # so the user sees the same condition that would fail _setup_install.
1042
- c.eprint(f"setup: warning: {exc}")
1053
+ eprint(f"setup: warning: {exc}")
1043
1054
  settings = {}
1044
1055
  detection = _setup_detect_legacy_bespoke_hooks(settings)
1045
1056
  sym_results = []
@@ -1238,7 +1249,7 @@ def _setup_install(args: argparse.Namespace) -> int:
1238
1249
 
1239
1250
  claude_dir = pathlib.Path.home() / ".claude"
1240
1251
  if not claude_dir.exists():
1241
- c.eprint(
1252
+ eprint(
1242
1253
  f"~/.claude/ does not exist. If Claude Code isn't installed yet, "
1243
1254
  f"install it first. If it is installed, run `claude` once to "
1244
1255
  f"initialize, then re-run cctally setup."
@@ -1259,7 +1270,7 @@ def _setup_install(args: argparse.Namespace) -> int:
1259
1270
  try:
1260
1271
  settings = c._load_claude_settings()
1261
1272
  except c.SetupError as exc:
1262
- c.eprint(f"setup: {exc}")
1273
+ eprint(f"setup: {exc}")
1263
1274
  return 1
1264
1275
 
1265
1276
  # ── Legacy bespoke hook detection + migration decision (spec §1, §2) ──
@@ -1304,7 +1315,7 @@ def _setup_install(args: argparse.Namespace) -> int:
1304
1315
  # still propagates into this code path post-extraction (§5.6 option C).
1305
1316
  backup_dir = c._legacy_resolve_backup_dir()
1306
1317
  except OSError as exc:
1307
- c.eprint(f"setup: cannot create migration backup dir: {exc}")
1318
+ eprint(f"setup: cannot create migration backup dir: {exc}")
1308
1319
  return 1
1309
1320
  # Unwire BEFORE the merge so the same atomic write removes legacy
1310
1321
  # entries and adds cctally entries (spec §2 step 6).
@@ -1322,14 +1333,14 @@ def _setup_install(args: argparse.Namespace) -> int:
1322
1333
  try:
1323
1334
  _settings_merge_install(settings, abs_path)
1324
1335
  except c.SetupError as exc:
1325
- c.eprint(f"setup: {exc}")
1336
+ eprint(f"setup: {exc}")
1326
1337
  return 1
1327
1338
 
1328
1339
  sym_results = _setup_create_symlinks(repo_root, dst_dir)
1329
1340
  failed = [r for r in sym_results if r.status == "failed"]
1330
1341
  if failed:
1331
1342
  for r in failed:
1332
- c.eprint(f"setup: symlink {r.name} failed: {r.detail}")
1343
+ eprint(f"setup: symlink {r.name} failed: {r.detail}")
1333
1344
  return 1
1334
1345
  new_count = sum(1 for r in sym_results if r.status == "created")
1335
1346
  same_count = sum(1 for r in sym_results if r.status == "already")
@@ -1356,7 +1367,7 @@ def _setup_install(args: argparse.Namespace) -> int:
1356
1367
  try:
1357
1368
  c._write_claude_settings_atomic(settings)
1358
1369
  except OSError as exc:
1359
- c.eprint(f"setup: failed to write {c.CLAUDE_SETTINGS_PATH}: {exc}")
1370
+ eprint(f"setup: failed to write {c.CLAUDE_SETTINGS_PATH}: {exc}")
1360
1371
  return 2
1361
1372
 
1362
1373
  # ── Post-write migration apply (spec §2 steps 6a, 6b) ──
@@ -1548,7 +1559,6 @@ def _setup_install(args: argparse.Namespace) -> int:
1548
1559
 
1549
1560
 
1550
1561
  def cmd_setup(args: argparse.Namespace) -> int:
1551
- c = _cctally()
1552
1562
  # Migration flags are install-mode-only. Reject combinations with
1553
1563
  # --status or --uninstall (per spec Section 2 mode×flag matrix). The
1554
1564
  # mutex group on the parser already prevents both flags being set
@@ -1560,7 +1570,7 @@ def cmd_setup(args: argparse.Namespace) -> int:
1560
1570
  else None
1561
1571
  )
1562
1572
  if mig_flag and (getattr(args, "status", False) or getattr(args, "uninstall", False)):
1563
- c.eprint(f"setup: {mig_flag} is install-mode only")
1573
+ eprint(f"setup: {mig_flag} is install-mode only")
1564
1574
  return 2
1565
1575
  if getattr(args, "uninstall", False):
1566
1576
  return _setup_uninstall(args)
@@ -30,12 +30,27 @@ def _cctally():
30
30
  return sys.modules["cctally"]
31
31
 
32
32
 
33
+ # === Honest imports from extracted homes ===================================
34
+ # Spec 2026-05-17-cctally-core-kernel-extraction.md §3.3: kernel symbols
35
+ # import from _cctally_core. `load_config` stays on the _cctally()
36
+ # accessor per spec §3.5 monkeypatch carve-out. Z-high helpers
37
+ # (`compute_week_cost`, `insert_cost_snapshot`) and out-of-scope
38
+ # (`pick_week_selection`) stay on the accessor per spec §3.7.
39
+ from _cctally_core import (
40
+ get_week_start_name,
41
+ open_db,
42
+ format_local_iso,
43
+ make_week_ref,
44
+ get_latest_usage_for_week,
45
+ )
46
+
47
+
33
48
  def cmd_sync_week(args: argparse.Namespace) -> int:
34
49
  c = _cctally()
35
50
  config = c.load_config()
36
- week_start_name = c.get_week_start_name(config, args.week_start_name)
51
+ week_start_name = get_week_start_name(config, args.week_start_name)
37
52
 
38
- conn = c.open_db()
53
+ conn = open_db()
39
54
  try:
40
55
  selection = c.pick_week_selection(
41
56
  conn,
@@ -54,8 +69,8 @@ def cmd_sync_week(args: argparse.Namespace) -> int:
54
69
  start_iso_override=selection.start_iso_override,
55
70
  end_iso_override=selection.end_iso_override,
56
71
  )
57
- week_start_at = selection.start_iso_override or c.format_local_iso(week_start, end_of_day=False)
58
- week_end_at = selection.end_iso_override or c.format_local_iso(week_end, end_of_day=True)
72
+ week_start_at = selection.start_iso_override or format_local_iso(week_start, end_of_day=False)
73
+ week_end_at = selection.end_iso_override or format_local_iso(week_end, end_of_day=True)
59
74
  insert_id = c.insert_cost_snapshot(
60
75
  conn,
61
76
  week_start=week_start,
@@ -69,13 +84,13 @@ def cmd_sync_week(args: argparse.Namespace) -> int:
69
84
  project=args.project,
70
85
  )
71
86
 
72
- week_ref = c.make_week_ref(
87
+ week_ref = make_week_ref(
73
88
  week_start_date=week_start.isoformat(),
74
89
  week_end_date=week_end.isoformat(),
75
90
  week_start_at=week_start_at,
76
91
  week_end_at=week_end_at,
77
92
  )
78
- usage_row = c.get_latest_usage_for_week(conn, week_ref)
93
+ usage_row = get_latest_usage_for_week(conn, week_ref)
79
94
  weekly_percent = float(usage_row["weekly_percent"]) if usage_row else None
80
95
  dollars_per_percent = (
81
96
  result.cost_usd / weekly_percent if weekly_percent and weekly_percent > 0 else None
@@ -202,43 +202,40 @@ def _cctally():
202
202
  return sys.modules["cctally"]
203
203
 
204
204
 
205
+ # === Honest imports from extracted homes ===================================
206
+ # Spec 2026-05-17-cctally-core-kernel-extraction.md §3.3.
207
+ from _cctally_core import (
208
+ eprint,
209
+ parse_iso_datetime,
210
+ _now_utc,
211
+ open_db,
212
+ get_latest_usage_for_week,
213
+ _canonicalize_optional_iso,
214
+ make_week_ref,
215
+ )
216
+ from _lib_display_tz import (
217
+ format_display_dt,
218
+ resolve_display_tz,
219
+ normalize_display_tz_value,
220
+ _compute_display_block,
221
+ )
222
+ from _lib_aggregators import _aggregate_monthly
223
+
224
+
205
225
  # === Module-level back-ref shims for helpers that STAY in bin/cctally ======
206
226
  # Each shim resolves ``sys.modules['cctally'].X`` at CALL TIME (not bind
207
227
  # time), so monkeypatches on cctally's namespace propagate into the moved
208
- # code unchanged. Mirrors the precedent established in
209
- # ``bin/_cctally_record.py``, ``bin/_cctally_cache.py``,
210
- # ``bin/_cctally_db.py``, ``bin/_cctally_update.py``, and
211
- # ``bin/_cctally_dashboard.py``.
212
- def eprint(*args, **kwargs):
213
- return sys.modules["cctally"].eprint(*args, **kwargs)
214
-
215
-
216
- def parse_iso_datetime(*args, **kwargs):
217
- return sys.modules["cctally"].parse_iso_datetime(*args, **kwargs)
218
-
219
-
220
- def _now_utc(*args, **kwargs):
221
- return sys.modules["cctally"]._now_utc(*args, **kwargs)
222
-
223
-
224
- def open_db(*args, **kwargs):
225
- return sys.modules["cctally"].open_db(*args, **kwargs)
226
-
227
-
228
+ # code unchanged. `load_config` and `get_claude_session_entries` STAY as
229
+ # shims even though their natural homes are decentralized (_cctally_config
230
+ # / _cctally_cache) — tests monkeypatch them via `ns["X"]` (21 sites total,
231
+ # audited 2026-05-17); direct imports would silently bypass the patches.
232
+ # See spec §3.5 (carve-out) and §3.7 (stays-on-shim allowlist).
228
233
  def load_config(*args, **kwargs):
229
234
  return sys.modules["cctally"].load_config(*args, **kwargs)
230
235
 
231
236
 
232
- def format_display_dt(*args, **kwargs):
233
- return sys.modules["cctally"].format_display_dt(*args, **kwargs)
234
-
235
-
236
- def resolve_display_tz(*args, **kwargs):
237
- return sys.modules["cctally"].resolve_display_tz(*args, **kwargs)
238
-
239
-
240
- def normalize_display_tz_value(*args, **kwargs):
241
- return sys.modules["cctally"].normalize_display_tz_value(*args, **kwargs)
237
+ def get_claude_session_entries(*args, **kwargs):
238
+ return sys.modules["cctally"].get_claude_session_entries(*args, **kwargs)
242
239
 
243
240
 
244
241
  def _resolve_display_tz_obj(*args, **kwargs):
@@ -253,10 +250,6 @@ def _apply_midweek_reset_override(*args, **kwargs):
253
250
  return sys.modules["cctally"]._apply_midweek_reset_override(*args, **kwargs)
254
251
 
255
252
 
256
- def _compute_display_block(*args, **kwargs):
257
- return sys.modules["cctally"]._compute_display_block(*args, **kwargs)
258
-
259
-
260
253
  def _compute_forecast(*args, **kwargs):
261
254
  return sys.modules["cctally"]._compute_forecast(*args, **kwargs)
262
255
 
@@ -297,18 +290,6 @@ def _aggregate_claude_sessions(*args, **kwargs):
297
290
  return sys.modules["cctally"]._aggregate_claude_sessions(*args, **kwargs)
298
291
 
299
292
 
300
- def _aggregate_monthly(*args, **kwargs):
301
- return sys.modules["cctally"]._aggregate_monthly(*args, **kwargs)
302
-
303
-
304
- def get_claude_session_entries(*args, **kwargs):
305
- return sys.modules["cctally"].get_claude_session_entries(*args, **kwargs)
306
-
307
-
308
- def get_latest_usage_for_week(*args, **kwargs):
309
- return sys.modules["cctally"].get_latest_usage_for_week(*args, **kwargs)
310
-
311
-
312
293
  def get_latest_cost_for_week(*args, **kwargs):
313
294
  return sys.modules["cctally"].get_latest_cost_for_week(*args, **kwargs)
314
295
 
@@ -317,10 +298,6 @@ def get_milestones_for_week(*args, **kwargs):
317
298
  return sys.modules["cctally"].get_milestones_for_week(*args, **kwargs)
318
299
 
319
300
 
320
- def _canonicalize_optional_iso(*args, **kwargs):
321
- return sys.modules["cctally"]._canonicalize_optional_iso(*args, **kwargs)
322
-
323
-
324
301
  def get_recent_weeks(*args, **kwargs):
325
302
  return sys.modules["cctally"].get_recent_weeks(*args, **kwargs)
326
303
 
@@ -1555,7 +1532,7 @@ def _tui_build_trend(
1555
1532
  canon_start, canon_end = c._get_canonical_boundary_for_date(
1556
1533
  conn, latest_usage["week_start_date"]
1557
1534
  )
1558
- current_ref = c.make_week_ref(
1535
+ current_ref = make_week_ref(
1559
1536
  week_start_date=latest_usage["week_start_date"],
1560
1537
  week_end_date=latest_usage["week_end_date"],
1561
1538
  week_start_at=canon_start,
@@ -192,28 +192,23 @@ def _cctally():
192
192
  return sys.modules["cctally"]
193
193
 
194
194
 
195
+ # === Honest imports from extracted homes ===================================
196
+ # Spec 2026-05-17-cctally-core-kernel-extraction.md §3.3.
197
+ from _cctally_core import eprint, _now_utc
198
+ from _cctally_config import save_config
199
+
200
+
195
201
  # === Module-level back-ref shims for helpers that STAY in bin/cctally ======
196
202
  # Each shim resolves ``sys.modules['cctally'].X`` at CALL TIME (not bind
197
203
  # time), so monkeypatches on cctally's namespace propagate into the moved
198
- # code unchanged. Mirrors the precedent established in
199
- # ``bin/_cctally_record.py`` (34 shims), ``bin/_cctally_cache.py``
200
- # (4 shims), and ``bin/_cctally_db.py`` (4 shims).
201
- def eprint(*args, **kwargs):
202
- return sys.modules["cctally"].eprint(*args, **kwargs)
203
-
204
-
205
- def _now_utc(*args, **kwargs):
206
- return sys.modules["cctally"]._now_utc(*args, **kwargs)
207
-
208
-
204
+ # code unchanged. `load_config` STAYS as a shim even though its natural
205
+ # home is _cctally_config — tests monkeypatch it via `ns["load_config"]`
206
+ # (16 sites, audited 2026-05-17); direct import would silently bypass.
207
+ # See spec §3.5 (carve-out) and §3.7 (stays-on-shim allowlist).
209
208
  def load_config(*args, **kwargs):
210
209
  return sys.modules["cctally"].load_config(*args, **kwargs)
211
210
 
212
211
 
213
- def save_config(*args, **kwargs):
214
- return sys.modules["cctally"].save_config(*args, **kwargs)
215
-
216
-
217
212
  def _release_read_latest_release_version(*args, **kwargs):
218
213
  return sys.modules["cctally"]._release_read_latest_release_version(
219
214
  *args, **kwargs
@@ -2114,7 +2109,7 @@ def _should_show_update_banner(
2114
2109
  return False
2115
2110
  if not config.get("update", {}).get("check", {}).get("enabled", True):
2116
2111
  return False
2117
- available, _ = c._compute_effective_update_available(state, suppress, c._now_utc())
2112
+ available, _ = c._compute_effective_update_available(state, suppress, _now_utc())
2118
2113
  return available
2119
2114
 
2120
2115
 
@@ -79,6 +79,13 @@ _lib_subscription_weeks = _load_lib("_lib_subscription_weeks")
79
79
  SubWeek = _lib_subscription_weeks.SubWeek
80
80
 
81
81
 
82
+ # === Honest imports from extracted homes ===================================
83
+ # Spec 2026-05-17-cctally-core-kernel-extraction.md §3.3: kernel symbols
84
+ # import from _cctally_core. `CODEX_SESSIONS_DIR` (path constant) and
85
+ # `_decode_escaped_cwd` (out-of-scope) stay on the _cctally() accessor.
86
+ from _cctally_core import parse_iso_datetime
87
+
88
+
82
89
  @dataclass
83
90
  class BucketUsage:
84
91
  """Aggregated usage for one time bucket.
@@ -247,7 +254,6 @@ def _aggregate_weekly(
247
254
  # candidate week in O(log W) per entry rather than the linear
248
255
  # scan that previously ran ~130k x ~54 = 7M comparisons.
249
256
  import bisect
250
- parse_iso_datetime = _cctally().parse_iso_datetime
251
257
  parsed_bounds: list[tuple[dt.datetime, dt.datetime, str]] = []
252
258
  for w in weeks:
253
259
  start_dt = parse_iso_datetime(w.start_ts, "week.start_ts")
@@ -116,11 +116,25 @@ _resolve_tz = _lib_display_tz._resolve_tz
116
116
  format_display_dt = _lib_display_tz.format_display_dt
117
117
 
118
118
 
119
- # Module-level back-ref shims. Each shim resolves
120
- # ``sys.modules['cctally'].X`` at CALL TIME (not bind time), so
121
- # monkeypatches on cctally's namespace propagate into the moved code
122
- # unchanged. Mirrors the precedent established in
123
- # ``bin/_lib_render.py`` / ``bin/_cctally_record.py``.
119
+ # === Honest imports from extracted homes ===================================
120
+ # Spec 2026-05-17-cctally-core-kernel-extraction.md §3.3: kernel symbols
121
+ # (Z-leaf + Z-mid) import from _cctally_core. The legacy shim functions
122
+ # for these names are deleted.
123
+ from _cctally_core import (
124
+ open_db,
125
+ _command_as_of,
126
+ _canonicalize_optional_iso,
127
+ parse_iso_datetime,
128
+ )
129
+
130
+
131
+ # === Module-level back-ref shims for helpers that STAY in bin/cctally ======
132
+ # Each shim resolves ``sys.modules['cctally'].X`` at CALL TIME (not bind
133
+ # time), so monkeypatches on cctally's namespace propagate into the moved
134
+ # code unchanged. `get_claude_session_entries` STAYS as a shim even though
135
+ # its natural home is _cctally_cache — tests monkeypatch it via ``ns["X"]``
136
+ # (audited 2026-05-17); a direct import would silently bypass the patches.
137
+ # See spec §3.5 (carve-out) and §3.7 (stays-on-shim allowlist).
124
138
  def get_claude_session_entries(*args, **kwargs):
125
139
  return sys.modules["cctally"].get_claude_session_entries(*args, **kwargs)
126
140
 
@@ -129,10 +143,6 @@ def _resolve_project_key(*args, **kwargs):
129
143
  return sys.modules["cctally"]._resolve_project_key(*args, **kwargs)
130
144
 
131
145
 
132
- def open_db(*args, **kwargs):
133
- return sys.modules["cctally"].open_db(*args, **kwargs)
134
-
135
-
136
146
  def _iso_z(*args, **kwargs):
137
147
  return sys.modules["cctally"]._iso_z(*args, **kwargs)
138
148
 
@@ -145,18 +155,6 @@ def _style_ansi(*args, **kwargs):
145
155
  return sys.modules["cctally"]._style_ansi(*args, **kwargs)
146
156
 
147
157
 
148
- def _command_as_of(*args, **kwargs):
149
- return sys.modules["cctally"]._command_as_of(*args, **kwargs)
150
-
151
-
152
- def _canonicalize_optional_iso(*args, **kwargs):
153
- return sys.modules["cctally"]._canonicalize_optional_iso(*args, **kwargs)
154
-
155
-
156
- def parse_iso_datetime(*args, **kwargs):
157
- return sys.modules["cctally"].parse_iso_datetime(*args, **kwargs)
158
-
159
-
160
158
  # Private eprint shim per spec §5.3 (pure layer does not back-import
161
159
  # cctally for ubiquitous helpers; eprint isn't actually called by the
162
160
  # moved code, but kept here as the canonical pure-layer pattern so