cctally 1.22.2 → 1.22.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.
@@ -115,6 +115,11 @@ _lib_display_tz = _load_lib("_lib_display_tz")
115
115
  _resolve_tz = _lib_display_tz._resolve_tz
116
116
  format_display_dt = _lib_display_tz.format_display_dt
117
117
 
118
+ # fmt/color/table primitives honest-imported from _lib_fmt (#126 C11)
119
+ _lib_fmt = _load_lib("_lib_fmt")
120
+ _style_ansi = _lib_fmt._style_ansi
121
+ _supports_unicode_stdout = _lib_fmt._supports_unicode_stdout
122
+
118
123
 
119
124
  # === Honest imports from extracted homes ===================================
120
125
  # Spec 2026-05-17-cctally-core-kernel-extraction.md §3.3: kernel symbols
@@ -147,14 +152,6 @@ def _iso_z(*args, **kwargs):
147
152
  return sys.modules["cctally"]._iso_z(*args, **kwargs)
148
153
 
149
154
 
150
- def _supports_unicode_stdout(*args, **kwargs):
151
- return sys.modules["cctally"]._supports_unicode_stdout(*args, **kwargs)
152
-
153
-
154
- def _style_ansi(*args, **kwargs):
155
- return sys.modules["cctally"]._style_ansi(*args, **kwargs)
156
-
157
-
158
155
  # Private eprint shim per spec §5.3 (pure layer does not back-import
159
156
  # cctally for ubiquitous helpers; eprint isn't actually called by the
160
157
  # moved code, but kept here as the canonical pure-layer pattern so
@@ -0,0 +1,325 @@
1
+ """Shared fmt / color / table render primitives (pure-fn kernel).
2
+
3
+ Holds the low-level formatting primitives extracted from bin/cctally
4
+ (#126, C11): timestamp/week-window compaction, color/unicode capability
5
+ detection, ANSI styling, display-width-aware boxed tables, number
6
+ formatting + width-budget truncation. Pure: the only environment reads
7
+ (os.environ, sys.stdout.isatty(), sys.stdout.encoding) happen INSIDE the
8
+ functions at call time, never at import.
9
+
10
+ Imported honestly by _lib_render.py and _lib_diff_kernel.py (via their
11
+ _load_lib helpers); re-exported on the bin/cctally namespace for the
12
+ _cctally_* command siblings and _lib_view_models.py (c.X accessor) and the
13
+ test monkeypatch surfaces.
14
+
15
+ Spec: docs/superpowers/specs/2026-06-01-extract-fmt-color-table-primitives-design.md
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import datetime as dt
20
+ import os
21
+ import pathlib
22
+ import re
23
+ import sys
24
+ import unicodedata
25
+ from typing import Any
26
+
27
+ # parse_iso_datetime: bare import matches the established _lib_* convention
28
+ # (_lib_aggregators.py:86, _lib_diff_kernel.py:123 do the same).
29
+ from _cctally_core import parse_iso_datetime
30
+
31
+
32
+ # format_display_dt: loaded via the file-path _load_lib helper, NOT a bare
33
+ # `from _lib_display_tz import …`. The repo's sibling loading deliberately
34
+ # bypasses sys.path (test/loader contexts may lack bin/ on the path); every
35
+ # _lib_* consumer of _lib_display_tz uses _load_lib (_lib_render.py:100,
36
+ # _lib_diff_kernel.py:114, _lib_aggregators.py:75). _lib_fmt matches them.
37
+ def _load_lib(name: str):
38
+ cached = sys.modules.get(name)
39
+ if cached is not None:
40
+ return cached
41
+ import importlib.util as _ilu
42
+ p = pathlib.Path(__file__).resolve().parent / f"{name}.py"
43
+ spec = _ilu.spec_from_file_location(name, p)
44
+ mod = _ilu.module_from_spec(spec)
45
+ sys.modules[name] = mod
46
+ spec.loader.exec_module(mod)
47
+ return mod
48
+
49
+
50
+ _lib_display_tz = _load_lib("_lib_display_tz")
51
+ format_display_dt = _lib_display_tz.format_display_dt
52
+
53
+
54
+ def _parse_iso_datetime_optional(value: Any) -> dt.datetime | None:
55
+ if not isinstance(value, str) or not value.strip():
56
+ return None
57
+ try:
58
+ return parse_iso_datetime(value, "timestamp")
59
+ except ValueError:
60
+ return None
61
+
62
+
63
+ def _format_ts_compact(
64
+ value: str | None,
65
+ tz: "ZoneInfo | None" = None,
66
+ ) -> str:
67
+ """Compact ISO-instant -> "YYYY-MM-DD HH:MM" line.
68
+
69
+ F5 fix: optional ``tz`` localizes the parsed instant before strftime
70
+ AND appends the offset suffix via ``display_tz_label`` so the column
71
+ becomes unambiguous (mirrors ``format_display_dt``'s pattern). When
72
+ ``tz`` is None, returns the legacy UTC-clock string with no suffix
73
+ so existing callers keep their byte-stable output.
74
+ """
75
+ parsed = _parse_iso_datetime_optional(value)
76
+ if parsed is None:
77
+ return "n/a"
78
+ if tz is None:
79
+ # Host-local fallback / default-config path: preserve the original
80
+ # byte-stable host-naive strftime output (UTC-aware datetimes render
81
+ # UTC clock, no suffix). This branch is reachable in production for
82
+ # users whose ``display.tz`` resolves to None — NOT a legacy or
83
+ # back-compat path. The non-None branch routes through
84
+ # ``format_display_dt`` for tz-aware rendering.
85
+ return parsed.strftime("%Y-%m-%d %H:%M")
86
+ return format_display_dt(parsed, tz, fmt="%Y-%m-%d %H:%M", suffix=True)
87
+
88
+
89
+ def _format_week_window(
90
+ week_start_date: str | None,
91
+ week_end_date: str | None,
92
+ week_start_at: str | None,
93
+ week_end_at: str | None,
94
+ tz: "ZoneInfo | None" = None,
95
+ ) -> str:
96
+ """Render a "<start> -> <end>" week-window column. F5 adds tz-aware
97
+ rendering for ISO-timestamp-bearing rows; legacy date-only rows pass
98
+ through unchanged. ``tz=None`` preserves byte-stable callers."""
99
+ if week_start_at and week_end_at:
100
+ return (
101
+ f"{_format_ts_compact(week_start_at, tz=tz)} -> "
102
+ f"{_format_ts_compact(week_end_at, tz=tz)}"
103
+ )
104
+ return f"{week_start_date or 'n/a'} -> {week_end_date or 'n/a'}"
105
+
106
+
107
+ def _supports_color_stdout() -> bool:
108
+ # Matches ccusage's picocolors behavior exactly.
109
+ # FORCE_COLOR always enables (any value, including empty)
110
+ if "FORCE_COLOR" in os.environ:
111
+ return True
112
+ # NO_COLOR always disables (any value, including empty)
113
+ if "NO_COLOR" in os.environ:
114
+ return False
115
+ # CI environments get color
116
+ if "CI" in os.environ:
117
+ return True
118
+ # TTY check on stdout or stderr
119
+ if sys.stdout.isatty() or sys.stderr.isatty():
120
+ term = os.environ.get("TERM", "")
121
+ return term.lower() != "dumb"
122
+ return False
123
+
124
+
125
+ def _style_ansi(text: str, code: str, enabled: bool) -> str:
126
+ if not enabled:
127
+ return text
128
+ return f"\033[{code}m{text}\033[0m"
129
+
130
+
131
+ def _supports_unicode_stdout() -> bool:
132
+ encoding = (sys.stdout.encoding or "").upper()
133
+ return "UTF" in encoding
134
+
135
+
136
+ def _display_width(s: str) -> int:
137
+ """Terminal cells consumed by ``s``.
138
+
139
+ Counts each codepoint by its East Asian Width: ``W`` / ``F`` (Wide
140
+ / Fullwidth) → 2 cells; combining marks → 0; everything else → 1.
141
+ Ambiguous (``A``) defaults to 1, matching every non-CJK terminal
142
+ locale — cctally has no CJK content in cell data, and `→` / `—` /
143
+ `·` (all `A`) are intentionally rendered narrow.
144
+
145
+ Used by `_boxed_table` so cells containing wide glyphs (notably
146
+ `⚡` U+26A1 on credit-row annotations) pad to the right cell count
147
+ rather than the right codepoint count. Without this, `len()`-based
148
+ padding under-pads by one cell per wide glyph and the right border
149
+ drifts off-column on those rows only.
150
+ """
151
+ width = 0
152
+ for ch in s:
153
+ if unicodedata.combining(ch):
154
+ continue
155
+ width += 2 if unicodedata.east_asian_width(ch) in ("W", "F") else 1
156
+ return width
157
+
158
+
159
+ def _boxed_table(
160
+ headers: list[str],
161
+ rows: list[list[str]],
162
+ aligns: list[str] | None = None,
163
+ *,
164
+ color_header: bool = True,
165
+ compact: bool = False,
166
+ ) -> str:
167
+ if not headers:
168
+ return ""
169
+ col_count = len(headers)
170
+ aligns = aligns or ["left"] * col_count
171
+ if len(aligns) != col_count:
172
+ raise ValueError("aligns length must match headers length")
173
+
174
+ sanitized_rows: list[list[str]] = []
175
+ for row in rows:
176
+ normalized = [str(cell).replace("\n", " ") for cell in row]
177
+ if len(normalized) != col_count:
178
+ raise ValueError("row length must match headers length")
179
+ sanitized_rows.append(normalized)
180
+
181
+ widths: list[int] = []
182
+ for idx, header in enumerate(headers):
183
+ max_cell = max((_display_width(r[idx]) for r in sanitized_rows), default=0)
184
+ widths.append(max(_display_width(header), max_cell))
185
+
186
+ def _pad(text: str, width: int, align: str) -> str:
187
+ deficit = width - _display_width(text)
188
+ if deficit <= 0:
189
+ return text
190
+ pad = " " * deficit
191
+ if align == "right":
192
+ return pad + text
193
+ if align == "center":
194
+ left = deficit // 2
195
+ return (" " * left) + text + (" " * (deficit - left))
196
+ return text + pad
197
+
198
+ if _supports_unicode_stdout():
199
+ chars = {
200
+ "top_left": "┌",
201
+ "top_mid": "┬",
202
+ "top_right": "┐",
203
+ "mid_left": "├",
204
+ "mid_mid": "┼",
205
+ "mid_right": "┤",
206
+ "bottom_left": "└",
207
+ "bottom_mid": "┴",
208
+ "bottom_right": "┘",
209
+ "h": "─",
210
+ "v": "│",
211
+ }
212
+ else:
213
+ chars = {
214
+ "top_left": "+",
215
+ "top_mid": "+",
216
+ "top_right": "+",
217
+ "mid_left": "+",
218
+ "mid_mid": "+",
219
+ "mid_right": "+",
220
+ "bottom_left": "+",
221
+ "bottom_mid": "+",
222
+ "bottom_right": "+",
223
+ "h": "-",
224
+ "v": "|",
225
+ }
226
+
227
+ color_enabled = _supports_color_stdout()
228
+
229
+ def _dim(s: str) -> str:
230
+ return _style_ansi(s, "90", color_enabled)
231
+
232
+ # Issue #91 (Shape B): ``compact`` drops the 1-space cell padding to
233
+ # 0 on this content-sized table (which has no proportional-width path
234
+ # to force). Borders and rows both key off ``pad`` so the default
235
+ # (``pad == 1``) reproduces the prior output byte-for-byte.
236
+ pad = 0 if compact else 1
237
+ pad_s = " " * pad
238
+
239
+ def make_border(left: str, mid: str, right: str) -> str:
240
+ return _dim(
241
+ left
242
+ + mid.join(chars["h"] * (w + 2 * pad) for w in widths)
243
+ + right
244
+ )
245
+
246
+ def make_row(cells: list[str], *, header: bool = False) -> str:
247
+ is_total = not header and cells and cells[0].strip() == "Total"
248
+ styled_cells: list[str] = []
249
+ for i, raw in enumerate(cells):
250
+ text = _pad(raw, widths[i], aligns[i])
251
+ if header and color_header:
252
+ text = _style_ansi(text, "36", color_enabled) # cyan text, like ccusage table head
253
+ elif is_total:
254
+ text = _style_ansi(text, "32", color_enabled) # green text for totals
255
+ styled_cells.append(text)
256
+ v = _dim(chars["v"])
257
+ return (
258
+ v
259
+ + pad_s
260
+ + f"{pad_s}{v}{pad_s}".join(styled_cells)
261
+ + pad_s
262
+ + v
263
+ )
264
+
265
+ top = make_border(chars["top_left"], chars["top_mid"], chars["top_right"])
266
+ mid = make_border(chars["mid_left"], chars["mid_mid"], chars["mid_right"])
267
+ bottom = make_border(chars["bottom_left"], chars["bottom_mid"], chars["bottom_right"])
268
+
269
+ out_lines = [top, make_row(headers, header=True), mid]
270
+ for idx, row in enumerate(sanitized_rows):
271
+ out_lines.append(make_row(row, header=False))
272
+ if idx < len(sanitized_rows) - 1:
273
+ out_lines.append(mid)
274
+ out_lines.append(bottom)
275
+ return "\n".join(out_lines)
276
+
277
+
278
+ def _fmt_num(n: int) -> str:
279
+ """Format integer with comma separators: 1234567 -> '1,234,567'."""
280
+ return f"{n:,}"
281
+
282
+
283
+ def _truncate_num(formatted: str, width: int) -> str:
284
+ """Truncate a formatted number to fit width, replacing tail with '…'."""
285
+ if len(formatted) <= width:
286
+ return formatted
287
+ return formatted[: width - 1] + "\u2026"
288
+
289
+
290
+ _ANSI_ESC_RE = re.compile(r"\033\[[0-9;]*m")
291
+
292
+
293
+ def _truncate_display(text: str, width: int) -> str:
294
+ """Truncate to `width` visible chars, preserving ANSI escape sequences.
295
+
296
+ Unlike `_truncate_num`, which slices raw string indices, this walks
297
+ the text treating `\\033[...m` sequences as zero-width and counts
298
+ printable chars toward the width budget. Used for left-aligned
299
+ cells that may carry a styled anomaly-glyph prefix — slicing those
300
+ with `_truncate_num` can cut through an ANSI escape and bleed
301
+ color into adjacent cells.
302
+ """
303
+ # Fast path: no ANSI codes, fall back to raw-slice truncation.
304
+ if "\033" not in text:
305
+ return _truncate_num(text, width)
306
+ stripped_len = len(_ANSI_ESC_RE.sub("", text))
307
+ if stripped_len <= width:
308
+ return text
309
+ # Walk chars until we've emitted (width - 1) visible chars, copying
310
+ # ANSI sequences verbatim. Append reset + ellipsis to close any open
311
+ # style and preserve the fit-to-width contract.
312
+ out: list[str] = []
313
+ visible = 0
314
+ i = 0
315
+ target = width - 1
316
+ while i < len(text) and visible < target:
317
+ m = _ANSI_ESC_RE.match(text, i)
318
+ if m:
319
+ out.append(m.group(0))
320
+ i = m.end()
321
+ continue
322
+ out.append(text[i])
323
+ visible += 1
324
+ i += 1
325
+ return "".join(out) + "\033[0m\u2026"
@@ -0,0 +1,182 @@
1
+ """Pure-fn kernel: the ccusage-parity "Pricing Mismatch Debug Report".
2
+
3
+ No I/O at import time; no import of `cctally`. The two cost primitives it
4
+ consumes (`_resolve_model_pricing`, `_calculate_entry_cost`) are honest-
5
+ imported from `_lib_pricing` (same `sys.modules` instance bin/cctally
6
+ re-exports). `UsageEntry` is duck-typed (attribute reads only). bin/cctally
7
+ re-exports every symbol below so internal call sites resolve unchanged.
8
+
9
+ Extracted from bin/cctally (#125 Batch E, C9). Spec:
10
+ docs/superpowers/specs/2026-06-01-extract-pricing-setup-glue-design.md
11
+ Original feature: issue #89 (ccusage detectMismatches/printMismatchReport).
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ import os
17
+ from dataclasses import dataclass, field
18
+
19
+ from _lib_pricing import _resolve_model_pricing, _calculate_entry_cost
20
+
21
+
22
+ @dataclass
23
+ class _MismatchModelStat:
24
+ total: int = 0
25
+ matches: int = 0
26
+ mismatches: int = 0
27
+ avg_percent_diff: float = 0.0
28
+
29
+
30
+ @dataclass
31
+ class _MismatchSample:
32
+ file: str
33
+ timestamp: str
34
+ model: str
35
+ original_cost: float
36
+ calculated_cost: float
37
+ difference: float
38
+ percent_diff: float
39
+ usage: dict
40
+
41
+
42
+ @dataclass
43
+ class _MismatchStats:
44
+ command_label: str | None = None
45
+ total_entries: int = 0
46
+ entries_with_both: int = 0
47
+ matches: int = 0
48
+ mismatches: int = 0
49
+ model_stats: dict = field(default_factory=dict)
50
+ discrepancies: list = field(default_factory=list)
51
+
52
+
53
+ def _compute_pricing_mismatch_stats(entries):
54
+ """Walk ``entries: Iterable[UsageEntry]`` and compute the mismatch stats
55
+ that ``_render_pricing_mismatch_report`` consumes.
56
+
57
+ Mirrors ccusage upstream's ``detectMismatches``
58
+ (``~/.npm/_npx/.../node_modules/ccusage/dist/debug-DvI5DUKR.js:6-95``):
59
+
60
+ - An entry counts toward ``entries_with_both`` iff its ``cost_usd``
61
+ is not None AND the model has pricing in ``CLAUDE_MODEL_PRICING``.
62
+ - Threshold: ``percent_diff < 0.1`` is a match; anything else is a
63
+ mismatch and gets appended to ``discrepancies`` in iteration order.
64
+ - ``percent_diff`` is ``0.0`` when recorded cost is zero (parity with
65
+ upstream's divide-by-zero guard).
66
+ - Per-model ``avg_percent_diff`` updated by streaming mean recurrence
67
+ to match upstream's per-row accumulation.
68
+ """
69
+ stats = _MismatchStats()
70
+ for entry in entries:
71
+ # P1.1 (issue #89 review-loop): mirror ccusage upstream's
72
+ # ``detectMismatches`` precondition filter at debug-DvI5DUKR.js:42
73
+ # — synthetic entries are excluded from total_entries AND skip the
74
+ # _resolve_model_pricing call (which would otherwise emit a
75
+ # ``[cost] unknown model: <synthetic>`` warning and mutate the
76
+ # module-level _unknown_model_warnings set, suppressing future
77
+ # legitimate emissions).
78
+ if entry.model == "<synthetic>":
79
+ continue
80
+ stats.total_entries += 1
81
+ if entry.cost_usd is None:
82
+ continue
83
+ if _resolve_model_pricing(entry.model) is None:
84
+ continue
85
+ stats.entries_with_both += 1
86
+ calculated = _calculate_entry_cost(
87
+ entry.model, entry.usage, mode="calculate",
88
+ )
89
+ original = float(entry.cost_usd)
90
+ difference = abs(original - calculated)
91
+ percent_diff = (difference / original * 100) if original > 0 else 0.0
92
+ ms = stats.model_stats.setdefault(entry.model, _MismatchModelStat())
93
+ ms.total += 1
94
+ if percent_diff < 0.1:
95
+ stats.matches += 1
96
+ ms.matches += 1
97
+ else:
98
+ stats.mismatches += 1
99
+ ms.mismatches += 1
100
+ stats.discrepancies.append(_MismatchSample(
101
+ file=os.path.basename(entry.source_path),
102
+ timestamp=entry.timestamp.isoformat(),
103
+ model=entry.model,
104
+ original_cost=original,
105
+ calculated_cost=calculated,
106
+ difference=difference,
107
+ percent_diff=percent_diff,
108
+ usage=dict(entry.usage),
109
+ ))
110
+ # Streaming-mean update for avg_percent_diff (matches upstream).
111
+ ms.avg_percent_diff = (
112
+ ms.avg_percent_diff * (ms.total - 1) + percent_diff
113
+ ) / ms.total
114
+ return stats
115
+
116
+
117
+ def _render_pricing_mismatch_report(stats, sample_limit):
118
+ """Return the report as a list of stderr lines (caller prints \\n-joined).
119
+
120
+ Matches ccusage upstream's ``printMismatchReport``
121
+ (debug-DvI5DUKR.js:97-145) including:
122
+ - Early-return ``"No pricing data found to analyze."`` when
123
+ ``entries_with_both == 0``.
124
+ - Model Statistics + Sample Discrepancies sections omitted when
125
+ ``mismatches == 0``.
126
+ - Models with ``mismatches == 0`` omitted from Model Statistics.
127
+ - Sample header prints the requested ``sample_limit`` (not min with
128
+ discrepancies length).
129
+ Adds ONE intentional non-upstream line: ``Command: cctally <label>``
130
+ under the header so the report self-identifies (issue #89 acceptance
131
+ re: "command in each sample's context").
132
+ """
133
+ out = []
134
+ if stats.entries_with_both == 0:
135
+ out.append("No pricing data found to analyze.")
136
+ return out
137
+
138
+ match_rate = stats.matches / stats.entries_with_both * 100
139
+ out.append("")
140
+ out.append("=== Pricing Mismatch Debug Report ===")
141
+ if stats.command_label:
142
+ out.append(f"Command: cctally {stats.command_label}")
143
+ out.append(f"Total entries processed: {stats.total_entries:,}")
144
+ out.append(
145
+ f"Entries with both costUSD and model: {stats.entries_with_both:,}"
146
+ )
147
+ out.append(f"Matches (within 0.1%): {stats.matches:,}")
148
+ out.append(f"Mismatches: {stats.mismatches:,}")
149
+ out.append(f"Match rate: {match_rate:.2f}%")
150
+
151
+ if stats.mismatches > 0 and stats.model_stats:
152
+ out.append("")
153
+ out.append("=== Model Statistics ===")
154
+ sorted_models = sorted(
155
+ stats.model_stats.items(),
156
+ key=lambda kv: -kv[1].mismatches,
157
+ )
158
+ for model, ms in sorted_models:
159
+ if ms.mismatches == 0:
160
+ continue
161
+ rate = ms.matches / ms.total * 100
162
+ out.append(f"{model}:")
163
+ out.append(f" Total entries: {ms.total:,}")
164
+ out.append(f" Matches: {ms.matches:,} ({rate:.1f}%)")
165
+ out.append(f" Mismatches: {ms.mismatches:,}")
166
+ out.append(f" Avg % difference: {ms.avg_percent_diff:.1f}%")
167
+
168
+ if stats.discrepancies and sample_limit > 0:
169
+ out.append("")
170
+ out.append(f"=== Sample Discrepancies (first {sample_limit}) ===")
171
+ for d in stats.discrepancies[:sample_limit]:
172
+ out.append(f"File: {d.file}")
173
+ out.append(f"Timestamp: {d.timestamp}")
174
+ out.append(f"Model: {d.model}")
175
+ out.append(f"Original cost: ${d.original_cost:.6f}")
176
+ out.append(f"Calculated cost: ${d.calculated_cost:.6f}")
177
+ out.append(
178
+ f"Difference: ${d.difference:.6f} ({d.percent_diff:.2f}%)"
179
+ )
180
+ out.append(f"Tokens: {json.dumps(d.usage)}")
181
+ out.append("---")
182
+ return out
@@ -100,36 +100,21 @@ _short_model_name = _lib_pricing._short_model_name
100
100
  _lib_display_tz = _load_lib("_lib_display_tz")
101
101
  _resolve_tz = _lib_display_tz._resolve_tz
102
102
 
103
+ # fmt/color/table primitives honest-imported from _lib_fmt (#126 C11)
104
+ _lib_fmt = _load_lib("_lib_fmt")
105
+ _supports_color_stdout = _lib_fmt._supports_color_stdout
106
+ _supports_unicode_stdout = _lib_fmt._supports_unicode_stdout
107
+ _style_ansi = _lib_fmt._style_ansi
108
+ _fmt_num = _lib_fmt._fmt_num
109
+ _truncate_num = _lib_fmt._truncate_num
110
+ _boxed_table = _lib_fmt._boxed_table
111
+
103
112
 
104
113
  # Module-level back-ref shims. Each shim resolves
105
114
  # ``sys.modules['cctally'].X`` at CALL TIME (not bind time), so
106
115
  # monkeypatches on cctally's namespace propagate into the moved code
107
116
  # unchanged. Mirrors the precedent established in
108
117
  # ``bin/_cctally_record.py`` / ``bin/_cctally_cache.py``.
109
- def _supports_color_stdout(*args, **kwargs):
110
- return sys.modules["cctally"]._supports_color_stdout(*args, **kwargs)
111
-
112
-
113
- def _supports_unicode_stdout(*args, **kwargs):
114
- return sys.modules["cctally"]._supports_unicode_stdout(*args, **kwargs)
115
-
116
-
117
- def _style_ansi(*args, **kwargs):
118
- return sys.modules["cctally"]._style_ansi(*args, **kwargs)
119
-
120
-
121
- def _fmt_num(*args, **kwargs):
122
- return sys.modules["cctally"]._fmt_num(*args, **kwargs)
123
-
124
-
125
- def _truncate_num(*args, **kwargs):
126
- return sys.modules["cctally"]._truncate_num(*args, **kwargs)
127
-
128
-
129
- def _boxed_table(*args, **kwargs):
130
- return sys.modules["cctally"]._boxed_table(*args, **kwargs)
131
-
132
-
133
118
  def _format_block_start(*args, **kwargs):
134
119
  return sys.modules["cctally"]._format_block_start(*args, **kwargs)
135
120
 
@@ -26,8 +26,8 @@ implicit), and `_compute_subscription_weeks` calls
26
26
  Moving both keeps the subscription-week domain self-contained and avoids
27
27
  inventing a call-time back-reference to `_apply_reset_events_to_subweeks`.
28
28
  `_apply_overlap_clamp_to_weekrefs` (operates on `WeekRef`, NOT `SubWeek`)
29
- stays in `bin/cctally` and reaches `_clamp_end_ats_to_next_start` through
30
- the re-export block.
29
+ lives in `bin/_cctally_weekrefs.py` and reaches `_clamp_end_ats_to_next_start`
30
+ through the cctally namespace (the re-export block + its call-time `c.` accessor).
31
31
 
32
32
  `bin/cctally` re-exports every public symbol below so the ~50 internal
33
33
  call sites + SourceFileLoader-based tests (`tests/test_subweek_display_dates`,