cctally 1.100.0 → 1.102.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.
- package/CHANGELOG.md +73 -0
- package/README.md +8 -2
- package/bin/_cctally_alerts.py +13 -2
- package/bin/_cctally_cache.py +3 -1
- package/bin/_cctally_cache_report.py +103 -6
- package/bin/_cctally_dashboard.py +1140 -282
- package/bin/_cctally_dashboard_conversation.py +12 -0
- package/bin/_cctally_dashboard_envelope.py +53 -61
- package/bin/_cctally_dashboard_share.py +101 -29
- package/bin/_cctally_dashboard_sources.py +663 -192
- package/bin/_cctally_diagnosis.py +1172 -0
- package/bin/_cctally_diagnosis_sources.py +4054 -0
- package/bin/_cctally_diff.py +20 -0
- package/bin/_cctally_forecast.py +329 -111
- package/bin/_cctally_milestone_history.py +10 -2
- package/bin/_cctally_parser.py +84 -0
- package/bin/_cctally_project.py +155 -47
- package/bin/_cctally_quota.py +14 -0
- package/bin/_cctally_record.py +151 -71
- package/bin/_cctally_refresh.py +105 -93
- package/bin/_cctally_share.py +9 -2
- package/bin/_cctally_source_analytics.py +40 -4
- package/bin/_cctally_statusline.py +8 -1
- package/bin/_cctally_tui.py +425 -234
- package/bin/_lib_alert_scope.py +685 -0
- package/bin/_lib_alerts_payload.py +112 -7
- package/bin/_lib_blocks.py +12 -0
- package/bin/_lib_cache_report.py +110 -1
- package/bin/_lib_codex_conversation.py +14 -0
- package/bin/_lib_codex_conversation_query.py +22 -8
- package/bin/_lib_codex_pools.py +20 -8
- package/bin/_lib_conversation.py +6 -3
- package/bin/_lib_conversation_query.py +256 -69
- package/bin/_lib_dashboard_sources.py +212 -24
- package/bin/_lib_diagnosis.py +1261 -0
- package/bin/_lib_forecast.py +62 -4
- package/bin/_lib_perf.py +12 -0
- package/bin/_lib_pricing.py +8 -7
- package/bin/_lib_readme_refresh.py +26 -5
- package/bin/_lib_render.py +31 -3
- package/bin/_lib_share_templates.py +150 -55
- package/bin/_lib_snapshot_cache.py +71 -13
- package/bin/_lib_source_identity.py +50 -2
- package/bin/_lib_subscription_weeks.py +65 -0
- package/bin/cctally +103 -16
- package/bin/cctally-explain +5 -0
- package/dashboard/static/assets/dashboardStream.shared-worker-1XTMV3nr.js +1 -0
- package/dashboard/static/assets/index-Di2hljvB.css +1 -0
- package/dashboard/static/assets/index-XYCIWjVG.js +97 -0
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +6 -1
- package/dashboard/static/assets/index-B5YfQEtn.css +0 -1
- package/dashboard/static/assets/index-Bt59nMMO.js +0 -97
package/bin/_cctally_parser.py
CHANGED
|
@@ -2385,6 +2385,89 @@ def _build_diff_parser(subparsers, name, *, help_text, xref=None, fixed_source=N
|
|
|
2385
2385
|
_add_account_arg(diff_p)
|
|
2386
2386
|
diff_p.set_defaults(func=c.cmd_diff)
|
|
2387
2387
|
|
|
2388
|
+
def _build_explain_parser(subparsers, name, *, help_text, xref=None):
|
|
2389
|
+
"""Build the `explain` parser (registered via _REGISTRATION; #620 S2).
|
|
2390
|
+
|
|
2391
|
+
`explain` deliberately does NOT adopt the shareable-output surface
|
|
2392
|
+
(`--format`/`--theme`/`--output`/`--copy`/`--open`). That surface is a
|
|
2393
|
+
separate concern with its own render kernel and byte-stability gates;
|
|
2394
|
+
only `--reveal-projects` is shared, because privacy is part of what the
|
|
2395
|
+
diagnosis publishes rather than part of how it is shared.
|
|
2396
|
+
"""
|
|
2397
|
+
c = _cctally()
|
|
2398
|
+
explain_p = subparsers.add_parser(
|
|
2399
|
+
name,
|
|
2400
|
+
help=help_text,
|
|
2401
|
+
formatter_class=CLIHelpFormatter,
|
|
2402
|
+
description=textwrap.dedent(
|
|
2403
|
+
"""\
|
|
2404
|
+
Explain where locally retained usage went, over seven
|
|
2405
|
+
contributor classes. Four are accounting-native: expensive model
|
|
2406
|
+
mix, one project dominating, one session dominating, and
|
|
2407
|
+
concentrated 5-hour bursts. Three are conversation-derived:
|
|
2408
|
+
prompt-cache churn, short conversations carrying large context,
|
|
2409
|
+
and subagent fan-out.
|
|
2410
|
+
|
|
2411
|
+
Each class reports a contributor when its top subject reaches the
|
|
2412
|
+
classification floor of its named denominator, reports no
|
|
2413
|
+
contributor when it is measured and nothing reaches the floor,
|
|
2414
|
+
and withholds with a typed cause when it cannot be measured. The
|
|
2415
|
+
classes overlap, so shares do not sum to 100 percent. Each class
|
|
2416
|
+
states the rule in force, so a verdict can be reproduced rather
|
|
2417
|
+
than inferred.
|
|
2418
|
+
|
|
2419
|
+
The three conversation-derived classes read the local transcript
|
|
2420
|
+
store, so they are withheld where it is absent or unreadable.
|
|
2421
|
+
Codex subagent attribution is derived from accounting metadata
|
|
2422
|
+
and needs no transcript access.
|
|
2423
|
+
|
|
2424
|
+
Exit codes: 0 for a valid report, including a healthy one and one
|
|
2425
|
+
whose fields are withheld; 2 for selector and usage errors; 3 for
|
|
2426
|
+
a report-establishment failure.
|
|
2427
|
+
"""
|
|
2428
|
+
),
|
|
2429
|
+
)
|
|
2430
|
+
explain_p.add_argument(
|
|
2431
|
+
# Defaulted to None rather than to the token itself, so the mutual
|
|
2432
|
+
# exclusion below can tell "the user asked for a window" from "nobody
|
|
2433
|
+
# said". `cmd_explain` resolves the absent case to `this-week`, which
|
|
2434
|
+
# is what keeps the default behaviour and the --help text unchanged.
|
|
2435
|
+
"--window", default=None,
|
|
2436
|
+
help="Window token (this-week | last-week | Nw-ago | this-month | "
|
|
2437
|
+
"last-month | Nm-ago | last-Nd | prev-Nd | "
|
|
2438
|
+
"YYYY-MM-DD..YYYY-MM-DD). Default: this-week.",
|
|
2439
|
+
)
|
|
2440
|
+
# Exact instants, because date grammar cannot express the window a warning
|
|
2441
|
+
# fired against: a five-hour block starts at an instant, not on a calendar
|
|
2442
|
+
# day. This is the pair the dashboard route already accepts, and the pair
|
|
2443
|
+
# every conversation-derived next step carries, so following one reproduces
|
|
2444
|
+
# the population the row measured rather than a day that contains it.
|
|
2445
|
+
explain_p.add_argument(
|
|
2446
|
+
"--start-at", default=None, metavar="ISO",
|
|
2447
|
+
help="Exact window start instant, inclusive (ISO 8601, e.g. "
|
|
2448
|
+
"2026-08-10T00:00:00Z). Requires --end-at and cannot be "
|
|
2449
|
+
"combined with --window.",
|
|
2450
|
+
)
|
|
2451
|
+
explain_p.add_argument(
|
|
2452
|
+
"--end-at", default=None, metavar="ISO",
|
|
2453
|
+
help="Exact window end instant, EXCLUSIVE (ISO 8601). Requires "
|
|
2454
|
+
"--start-at and cannot be combined with --window.",
|
|
2455
|
+
)
|
|
2456
|
+
explain_p.add_argument("--json", dest="emit_json", action="store_true",
|
|
2457
|
+
help="Emit the stamped JSON envelope instead of the terminal report.")
|
|
2458
|
+
explain_p.add_argument(
|
|
2459
|
+
"--reveal-projects", action="store_true", dest="reveal_projects",
|
|
2460
|
+
help="Show derived project display labels instead of the default "
|
|
2461
|
+
"project-1, project-2, ... anonymization. Never shows "
|
|
2462
|
+
"filesystem paths, and never widens session identities.",
|
|
2463
|
+
)
|
|
2464
|
+
explain_p.add_argument("--tz", default=None, type=_argparse_tz, metavar="TZ",
|
|
2465
|
+
help="Display timezone: local, utc, or IANA name. "
|
|
2466
|
+
"Overrides config display.tz for this call.")
|
|
2467
|
+
_add_source_args(explain_p, speed=True)
|
|
2468
|
+
_add_account_arg(explain_p)
|
|
2469
|
+
explain_p.set_defaults(func=c.cmd_explain)
|
|
2470
|
+
|
|
2388
2471
|
def _build_claude_parser(subparsers, name, *, help_text, xref=None):
|
|
2389
2472
|
"""Build the `claude` parser (registered via _REGISTRATION; #279 S6 W3).
|
|
2390
2473
|
|
|
@@ -3743,6 +3826,7 @@ _REGISTRATION = (
|
|
|
3743
3826
|
_Reg('alerts', _build_alerts_parser, "Manage threshold alerts", None, None),
|
|
3744
3827
|
_Reg('setup', _build_setup_parser, "Install provider hooks/handlers + symlinks", None, None),
|
|
3745
3828
|
_Reg('db', _build_db_parser, "Migration / DB management (status, skip, unskip)", None, None),
|
|
3829
|
+
_Reg('explain', _build_explain_parser, "Explain where locally retained usage went, ranked with evidence", None, None),
|
|
3746
3830
|
_Reg('doctor', _build_doctor_parser, "Diagnose data freshness and install state", None, None),
|
|
3747
3831
|
_Reg('dashboard-perf', _build_dashboard_perf_parser, "Read a running dashboard's tick cost; arm its phase trace", None, None),
|
|
3748
3832
|
_Reg('pricing-check', _build_pricing_check_parser, "Detect stale or missing embedded model pricing", None, None),
|
package/bin/_cctally_project.py
CHANGED
|
@@ -24,6 +24,7 @@ import bisect
|
|
|
24
24
|
import datetime as dt
|
|
25
25
|
import json
|
|
26
26
|
import os
|
|
27
|
+
import sqlite3
|
|
27
28
|
import sys
|
|
28
29
|
|
|
29
30
|
from _cctally_core import (
|
|
@@ -35,6 +36,9 @@ from _cctally_core import (
|
|
|
35
36
|
)
|
|
36
37
|
from _lib_fmt import stable_sum
|
|
37
38
|
|
|
39
|
+
# #620 S1 D11: the one affordance shape every warning state renders.
|
|
40
|
+
import _lib_alert_scope
|
|
41
|
+
|
|
38
42
|
|
|
39
43
|
def _cctally():
|
|
40
44
|
"""Resolve the current `cctally` module at call-time (spec §3.1)."""
|
|
@@ -42,7 +46,8 @@ def _cctally():
|
|
|
42
46
|
|
|
43
47
|
|
|
44
48
|
def _load_week_snapshots(
|
|
45
|
-
since: dt.datetime, until: dt.datetime
|
|
49
|
+
since: dt.datetime, until: dt.datetime, *,
|
|
50
|
+
account_key: "str | None" = None,
|
|
46
51
|
) -> dict[dt.datetime, float]:
|
|
47
52
|
"""Return {week_start_utc -> max(weekly_percent)} for weeks intersecting
|
|
48
53
|
the [since, until] range.
|
|
@@ -68,20 +73,43 @@ def _load_week_snapshots(
|
|
|
68
73
|
helper apply (spec §4a, test S15). The floor compare uses `unixepoch()` on
|
|
69
74
|
both sides (mixed `Z` / `+00:00` offset spellings).
|
|
70
75
|
|
|
76
|
+
``account_key`` (#620 S1): scopes the read to one account's snapshots.
|
|
77
|
+
``None`` — the default and every unfiltered invocation — is the merged
|
|
78
|
+
all-accounts read, byte-identical to before. A real key is required under
|
|
79
|
+
``--account``: two accounts hold independent weekly quotas, so summing
|
|
80
|
+
another account's percentage into an account-filtered total reports a
|
|
81
|
+
figure that describes neither account. Percentages across accounts are
|
|
82
|
+
never summed for the same reason the dashboard never sums them.
|
|
83
|
+
|
|
71
84
|
Returns an empty dict if the stats DB has no relevant rows.
|
|
72
85
|
"""
|
|
86
|
+
acct_pred = "" if account_key is None else " AND account_key = ?"
|
|
87
|
+
acct_params: tuple = () if account_key is None else (account_key,)
|
|
73
88
|
conn = open_db()
|
|
74
89
|
try:
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
90
|
+
try:
|
|
91
|
+
cur = conn.execute(
|
|
92
|
+
"SELECT week_start_date, week_start_at, week_end_at, "
|
|
93
|
+
" captured_at_utc, weekly_percent "
|
|
94
|
+
"FROM weekly_usage_snapshots "
|
|
95
|
+
"WHERE week_start_at IS NOT NULL "
|
|
96
|
+
"AND week_end_at IS NOT NULL "
|
|
97
|
+
"AND datetime(week_start_at) < datetime(?) "
|
|
98
|
+
"AND datetime(week_end_at) > datetime(?)"
|
|
99
|
+
f"{acct_pred}",
|
|
100
|
+
(until.isoformat(), since.isoformat(), *acct_params),
|
|
101
|
+
)
|
|
102
|
+
except sqlite3.OperationalError:
|
|
103
|
+
# A stats.db predating the `account_key` column — an install
|
|
104
|
+
# whose migrations have not run, or a dev binary that refused
|
|
105
|
+
# the prod forward-migration (#142). `_projects_week_grid`
|
|
106
|
+
# carries the same guard for the same predicate.
|
|
107
|
+
#
|
|
108
|
+
# Withholding is the degradation, not dropping the predicate:
|
|
109
|
+
# retrying unscoped would sum another account's weekly
|
|
110
|
+
# percentage into an account-filtered total, which is the
|
|
111
|
+
# figure describing neither account that #620 S1 removed.
|
|
112
|
+
return {}
|
|
85
113
|
rows_in = []
|
|
86
114
|
for wsd, ws_iso, we_iso, cap_iso, pct in cur.fetchall():
|
|
87
115
|
if ws_iso is None or pct is None:
|
|
@@ -402,46 +430,115 @@ def cmd_project(args: argparse.Namespace) -> int:
|
|
|
402
430
|
now = _command_as_of()
|
|
403
431
|
conn = open_db()
|
|
404
432
|
|
|
405
|
-
#
|
|
406
|
-
#
|
|
407
|
-
#
|
|
408
|
-
#
|
|
433
|
+
# #341 --account: resolve the render filter (provider=claude; fail closed
|
|
434
|
+
# with exit 3 when the entry cache is unavailable). None = merged.
|
|
435
|
+
#
|
|
436
|
+
# #620 S1 D1: resolved BEFORE the subscription-week walk below, not after
|
|
437
|
+
# it. `_compute_subscription_weeks` takes an account context precisely so
|
|
438
|
+
# one account's resets cannot re-anchor another's week walk, and passing
|
|
439
|
+
# `None` while `--account` was in force bucketed one account's dollars
|
|
440
|
+
# against the merged boundary set — which, when two accounts reset on
|
|
441
|
+
# different weekdays, is neither account's. An invocation WITHOUT
|
|
442
|
+
# `--account` still passes `None` and keeps today's merged boundaries.
|
|
443
|
+
# Argument VALIDATION runs first. Task 4 moved `resolve_account_filter`
|
|
444
|
+
# ahead of the range resolution because `_compute_subscription_weeks`
|
|
445
|
+
# needs the account context to build the default window, and that
|
|
446
|
+
# reordering also moved which error a user sees when both are wrong: a
|
|
447
|
+
# malformed `--since` with an unavailable entry cache started exiting 3
|
|
448
|
+
# instead of 2. Exit codes are contract (`docs/cli-contract.md`), and a
|
|
449
|
+
# native-usage error outranks a staged environment failure because it is
|
|
450
|
+
# the one the user can act on. Only the parse moves back in front; the
|
|
451
|
+
# resolved account still precedes interval construction below.
|
|
409
452
|
source_range = getattr(args, "_source_analytics_range", None)
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
elif args.since or args.until:
|
|
453
|
+
parsed_explicit_range = None
|
|
454
|
+
if source_range is None and (args.since or args.until):
|
|
413
455
|
parsed = c._parse_cli_date_range(args, now_utc=now)
|
|
414
456
|
if isinstance(parsed, int):
|
|
415
457
|
# Translate the ccusage-parity helper's exit 1 into project's own
|
|
416
458
|
# native usage code 2 — project is cctally-native, not a ccusage
|
|
417
459
|
# drop-in (docs/cli-contract.md; #279 S6 W2).
|
|
418
460
|
return 2
|
|
419
|
-
|
|
461
|
+
parsed_explicit_range = parsed
|
|
462
|
+
|
|
463
|
+
acct_key, acct_exit = c.resolve_account_filter(
|
|
464
|
+
args, "claude", needs_cache=True)
|
|
465
|
+
if acct_exit is not None:
|
|
466
|
+
return acct_exit
|
|
467
|
+
|
|
468
|
+
# Resolve [since_dt, until_dt] in UTC. All-provider project dispatch
|
|
469
|
+
# resolves its calendar range once, then injects it here so Claude and
|
|
470
|
+
# Codex describe precisely the same interval. The normal Claude parser
|
|
471
|
+
# remains unchanged for every user-facing legacy invocation.
|
|
472
|
+
if source_range is not None:
|
|
473
|
+
since_dt, until_dt = source_range
|
|
474
|
+
elif parsed_explicit_range is not None:
|
|
475
|
+
since_dt, until_dt = parsed_explicit_range
|
|
420
476
|
since_dt = since_dt.astimezone(dt.timezone.utc)
|
|
421
477
|
until_dt = until_dt.astimezone(dt.timezone.utc)
|
|
422
478
|
else:
|
|
423
|
-
# Default to the current subscription week; --weeks N extends
|
|
479
|
+
# Default to the current subscription week; --weeks N extends
|
|
480
|
+
# backwards over the REAL intervals.
|
|
481
|
+
#
|
|
482
|
+
# #620 S1. `since_dt = cw_start - 7 * (weeks - 1)` was short of the
|
|
483
|
+
# truth by the accumulated shortfall of every drifted week in the
|
|
484
|
+
# window — one day per six-day week, several days over `--weeks 12`.
|
|
485
|
+
# `_compute_subscription_weeks` emits a leading extrapolated slice
|
|
486
|
+
# covering whatever `since_dt` it is given, so that deficit was not
|
|
487
|
+
# empty: every entry inside it was read and bucketed into a week the
|
|
488
|
+
# panel does not have, `_load_week_snapshots` admitted the prior real
|
|
489
|
+
# week (it selects every week OVERLAPPING the range) and summed its
|
|
490
|
+
# whole percentage into `totals.usedPercent`, and the extra week with
|
|
491
|
+
# no snapshot drove the rendered line "Used % unavailable for 1 week".
|
|
492
|
+
#
|
|
493
|
+
# The window is resolved the way the dashboard Projects panel
|
|
494
|
+
# resolves it: one walk over one shared probe range, locate the
|
|
495
|
+
# interval containing `now`, then take the `weeks` intervals ending
|
|
496
|
+
# there. `rangeStart` and `weeksInRange` move as a result, which is
|
|
497
|
+
# the point — a figure computed over the wrong population is what
|
|
498
|
+
# this correction removes.
|
|
499
|
+
weeks_back = args.weeks if args.weeks is not None else 1
|
|
500
|
+
probe_start, _probe_end = c.subscription_window_probe_range(
|
|
501
|
+
now, weeks_back,
|
|
502
|
+
)
|
|
424
503
|
# Widen by 1us so the emit loop fires when `now` is exactly at a reset
|
|
425
504
|
# boundary (zero-width [now, now] makes Case A's `current < range_end`
|
|
426
505
|
# false, which would otherwise wrongly fall through to the Monday
|
|
427
506
|
# fallback for non-Monday-reset accounts).
|
|
428
|
-
|
|
429
|
-
conn,
|
|
430
|
-
|
|
507
|
+
probe_weeks = c._compute_subscription_weeks(
|
|
508
|
+
conn, probe_start, now + dt.timedelta(microseconds=1),
|
|
509
|
+
config=config,
|
|
510
|
+
# #341/#620 S1: the resolved `--account` scope, or None for the
|
|
511
|
+
# merged (all-accounts) analytics read.
|
|
512
|
+
account_key=acct_key,
|
|
431
513
|
)
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
514
|
+
probe_bounds: list[tuple[dt.datetime, dt.datetime]] = []
|
|
515
|
+
for sw in probe_weeks:
|
|
516
|
+
probe_bounds.append((
|
|
517
|
+
parse_iso_datetime(sw.start_ts, "week.start_ts").astimezone(
|
|
518
|
+
dt.timezone.utc),
|
|
519
|
+
parse_iso_datetime(sw.end_ts, "week.end_ts").astimezone(
|
|
520
|
+
dt.timezone.utc),
|
|
521
|
+
))
|
|
522
|
+
cw_start = None
|
|
523
|
+
for s_dt, e_dt in reversed(probe_bounds):
|
|
524
|
+
if s_dt <= now < e_dt:
|
|
525
|
+
cw_start = s_dt
|
|
526
|
+
window = (
|
|
527
|
+
c.subscription_window_ending_at(probe_bounds, cw_start, weeks_back)
|
|
528
|
+
if cw_start is not None else []
|
|
529
|
+
)
|
|
530
|
+
if window:
|
|
531
|
+
since_dt = window[0][0]
|
|
443
532
|
else:
|
|
444
|
-
|
|
533
|
+
# No interval covers `now` — the genuine no-anchor path. A
|
|
534
|
+
# Monday-anchored week stepped back in seven-day multiples is
|
|
535
|
+
# what that history actually looks like, and it is what the
|
|
536
|
+
# panel falls back to as well.
|
|
537
|
+
if cw_start is None:
|
|
538
|
+
cw_start = (now - dt.timedelta(days=now.weekday())).replace(
|
|
539
|
+
hour=0, minute=0, second=0, microsecond=0
|
|
540
|
+
)
|
|
541
|
+
since_dt = cw_start - dt.timedelta(days=7 * (weeks_back - 1))
|
|
445
542
|
until_dt = now
|
|
446
543
|
|
|
447
544
|
# Pre-compute subscription-week bounds for the query window so each entry
|
|
@@ -449,7 +546,9 @@ def cmd_project(args: argparse.Namespace) -> int:
|
|
|
449
546
|
# `_aggregate_weekly`'s bisect pattern (first-match-wins on overlap).
|
|
450
547
|
subweeks = c._compute_subscription_weeks(
|
|
451
548
|
conn, since_dt, until_dt, config=config,
|
|
452
|
-
|
|
549
|
+
# #341/#620 S1: the resolved `--account` scope, or None for the merged
|
|
550
|
+
# (all-accounts) analytics read.
|
|
551
|
+
account_key=acct_key,
|
|
453
552
|
)
|
|
454
553
|
parsed_bounds: list[tuple[dt.datetime, dt.datetime]] = []
|
|
455
554
|
for sw in subweeks:
|
|
@@ -495,13 +594,6 @@ def cmd_project(args: argparse.Namespace) -> int:
|
|
|
495
594
|
else:
|
|
496
595
|
scan_start, scan_end = since_dt, until_dt
|
|
497
596
|
|
|
498
|
-
# #341 --account: resolve the render filter (provider=claude; fail closed
|
|
499
|
-
# with exit 3 when the entry cache is unavailable). None = merged.
|
|
500
|
-
acct_key, acct_exit = c.resolve_account_filter(
|
|
501
|
-
args, "claude", needs_cache=True)
|
|
502
|
-
if acct_exit is not None:
|
|
503
|
-
return acct_exit
|
|
504
|
-
|
|
505
597
|
resolver_cache: dict[str, ProjectKey] = {}
|
|
506
598
|
buckets: dict[tuple[ProjectKey, dt.datetime], dict] = {}
|
|
507
599
|
total_cost_by_week: dict[dt.datetime, float] = {}
|
|
@@ -622,16 +714,32 @@ def cmd_project(args: argparse.Namespace) -> int:
|
|
|
622
714
|
buckets[bkey] = b
|
|
623
715
|
_accumulate_entry_into_bucket(b, entry, pre_computed_cost=entry_cost)
|
|
624
716
|
|
|
717
|
+
# The remediation moved OUT of these two sentences and into the shared
|
|
718
|
+
# affordance line below (#620 S1 D11), so the terminal states the problem
|
|
719
|
+
# once and the fix once. The `warnings` list the JSON payload carries is a
|
|
720
|
+
# separate wire surface and keeps its own wording unchanged.
|
|
625
721
|
if unknown_entry_count > 0:
|
|
626
722
|
eprint(
|
|
627
|
-
f"Warning: {unknown_entry_count} entries lacked project_path
|
|
628
|
-
f"run `cache-sync` to backfill."
|
|
723
|
+
f"Warning: {unknown_entry_count} entries lacked project_path."
|
|
629
724
|
)
|
|
630
725
|
if missing_sid_count > 0:
|
|
631
726
|
eprint(
|
|
632
727
|
f"Warning: {missing_sid_count} entries lacked session_files "
|
|
633
|
-
f"session_id
|
|
728
|
+
f"session_id."
|
|
634
729
|
)
|
|
730
|
+
# #620 S1 D11: state the remediation in the one shape every warning uses,
|
|
731
|
+
# scoped to the provider whose cache is short and the range that was read.
|
|
732
|
+
# The `warnings` list the JSON payload carries is deliberately untouched;
|
|
733
|
+
# it is a wire surface, and this is the terminal affordance.
|
|
734
|
+
resolved_source = getattr(args, "source", None) or "claude"
|
|
735
|
+
if unknown_entry_count > 0 or missing_sid_count > 0:
|
|
736
|
+
eprint(_lib_alert_scope.next_step_line(
|
|
737
|
+
f"cctally cache-sync --source {resolved_source}",
|
|
738
|
+
provider=resolved_source,
|
|
739
|
+
window_start=since_dt,
|
|
740
|
+
window_end=until_dt,
|
|
741
|
+
tz=getattr(args, "_resolved_tz", None),
|
|
742
|
+
))
|
|
635
743
|
|
|
636
744
|
# --- Attribution math (Task 5) -----------------------------------------
|
|
637
745
|
# Load per-week `weekly_percent` (max within window) for every week that
|
|
@@ -639,7 +747,7 @@ def cmd_project(args: argparse.Namespace) -> int:
|
|
|
639
747
|
# can surface `weeksMissingSnapshot` in the output — those weeks can't
|
|
640
748
|
# contribute to attributed %.
|
|
641
749
|
week_snapshots: dict[dt.datetime, float] = _load_week_snapshots(
|
|
642
|
-
since_dt, until_dt
|
|
750
|
+
since_dt, until_dt, account_key=acct_key,
|
|
643
751
|
)
|
|
644
752
|
|
|
645
753
|
# Set of every week the user asked about (from the computed SubWeek
|
package/bin/_cctally_quota.py
CHANGED
|
@@ -194,6 +194,7 @@ PROJECTION_READ_CHOKEPOINTS: "frozenset[str]" = frozenset({
|
|
|
194
194
|
"_cctally_quota.py::_orphan_unseen::quota_window_blocks",
|
|
195
195
|
"_cctally_quota.py::_orphan_unseen_scoped::quota_window_blocks",
|
|
196
196
|
"_cctally_quota.py::_apply_quota_projection_rows::quota_projection_state",
|
|
197
|
+
"_cctally_diagnosis_sources.py::<module>::quota_window_blocks",
|
|
197
198
|
})
|
|
198
199
|
|
|
199
200
|
#: What each enumerated site does about the gate.
|
|
@@ -246,6 +247,10 @@ PROJECTION_READ_SITE_ACTIONS: "dict[str, str]" = {
|
|
|
246
247
|
"projector",
|
|
247
248
|
"_cctally_quota.py::_apply_quota_projection_rows::quota_projection_state":
|
|
248
249
|
"projector",
|
|
250
|
+
# `cctally explain` holds its Codex block SQL as a module-level constant,
|
|
251
|
+
# so the function that executes it gates instead.
|
|
252
|
+
"_cctally_diagnosis_sources.py::<module>::quota_window_blocks":
|
|
253
|
+
"gate_at_caller",
|
|
249
254
|
}
|
|
250
255
|
|
|
251
256
|
#: For each `gate_at_caller` site, the `<file>::<function>` callers that run the
|
|
@@ -274,6 +279,15 @@ PROJECTION_GATE_CALLERS: "dict[str, tuple[str, ...]]" = {
|
|
|
274
279
|
"_cctally_tui.py::_tui_compute_dispatch_signature",
|
|
275
280
|
"_cctally_dashboard_sources.py::_codex_quota_reuse_identity",
|
|
276
281
|
),
|
|
282
|
+
# Two functions in that module execute module-level SQL against
|
|
283
|
+
# `quota_window_blocks` — the block read and the observation-horizon read —
|
|
284
|
+
# and the scanner sees both as the one `<module>` site. Both are named,
|
|
285
|
+
# because "the horizon read is safe today because of call order" is not a
|
|
286
|
+
# property this guard can check, and a reordering would not fail it.
|
|
287
|
+
"_cctally_diagnosis_sources.py::<module>::quota_window_blocks": (
|
|
288
|
+
"_cctally_diagnosis_sources.py::_read_codex_blocks",
|
|
289
|
+
"_cctally_diagnosis_sources.py::_read_store_horizon",
|
|
290
|
+
),
|
|
277
291
|
}
|
|
278
292
|
|
|
279
293
|
#: Dynamic-target read sites per file — a `FROM`/`JOIN` whose target is
|