cctally 1.65.0 → 1.67.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 (54) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.md +6 -4
  3. package/bin/_cctally_alerts.py +1 -1
  4. package/bin/_cctally_cache.py +203 -22
  5. package/bin/_cctally_cache_report.py +7 -5
  6. package/bin/_cctally_core.py +125 -14
  7. package/bin/_cctally_dashboard.py +495 -4893
  8. package/bin/_cctally_dashboard_cache_report.py +714 -0
  9. package/bin/_cctally_dashboard_conversation.py +830 -0
  10. package/bin/_cctally_dashboard_envelope.py +1520 -0
  11. package/bin/_cctally_dashboard_share.py +2012 -0
  12. package/bin/_cctally_db.py +158 -5
  13. package/bin/_cctally_doctor.py +104 -3
  14. package/bin/_cctally_five_hour.py +19 -3
  15. package/bin/_cctally_forecast.py +118 -161
  16. package/bin/_cctally_parser.py +1003 -777
  17. package/bin/_cctally_percent_breakdown.py +1 -1
  18. package/bin/_cctally_pricing_check.py +39 -0
  19. package/bin/_cctally_project.py +18 -42
  20. package/bin/_cctally_record.py +279 -274
  21. package/bin/_cctally_reporting.py +1 -1
  22. package/bin/_cctally_setup.py +150 -43
  23. package/bin/_cctally_sync_week.py +1 -1
  24. package/bin/_cctally_telemetry.py +3 -5
  25. package/bin/_cctally_transcript.py +191 -0
  26. package/bin/_cctally_tui.py +42 -18
  27. package/bin/_lib_alert_dispatch.py +1 -1
  28. package/bin/_lib_blocks.py +6 -1
  29. package/bin/_lib_conversation_anon.py +254 -0
  30. package/bin/_lib_conversation_query.py +66 -0
  31. package/bin/_lib_credit.py +133 -0
  32. package/bin/_lib_diff_kernel.py +19 -7
  33. package/bin/_lib_doctor.py +150 -1
  34. package/bin/_lib_five_hour.py +61 -0
  35. package/bin/_lib_forecast.py +144 -0
  36. package/bin/_lib_json_envelope.py +38 -0
  37. package/bin/_lib_jsonl.py +115 -73
  38. package/bin/_lib_log.py +96 -0
  39. package/bin/_lib_pricing.py +47 -1
  40. package/bin/_lib_pricing_check.py +25 -3
  41. package/bin/_lib_record.py +179 -0
  42. package/bin/_lib_render.py +26 -11
  43. package/bin/_lib_snapshot_cache.py +61 -0
  44. package/bin/_lib_view_models.py +7 -1
  45. package/bin/cctally +59 -7
  46. package/bin/cctally-dashboard +2 -2
  47. package/bin/cctally-statusline +1 -0
  48. package/bin/cctally-transcript +5 -0
  49. package/bin/cctally-tui +2 -2
  50. package/dashboard/static/assets/index-BybNp_Di.js +80 -0
  51. package/dashboard/static/assets/{index-DQWNrIqu.css → index-DgAmLK65.css} +1 -1
  52. package/dashboard/static/dashboard.html +2 -2
  53. package/package.json +13 -1
  54. package/dashboard/static/assets/index-CJTUCpKt.js +0 -80
@@ -156,7 +156,7 @@ def cmd_percent_breakdown(args: argparse.Namespace) -> int:
156
156
  }
157
157
 
158
158
  if args.json:
159
- print(json.dumps(output, indent=2))
159
+ print(json.dumps(c.stamp_schema_version(output), indent=2))
160
160
  return 0
161
161
 
162
162
  # Prefer the reset-adjusted ISO timestamps in the terminal header
@@ -283,6 +283,14 @@ def cmd_pricing_check(args: argparse.Namespace) -> int:
283
283
 
284
284
  drift = {"value_drift": [], "missing_from_us": [], "ahead_of_litellm": []}
285
285
  existence = {"status": "skipped", "unpriced_vendor_models": []}
286
+ # staleSuppressions is network-derived (needs the LiteLLM scoped table), so
287
+ # under --offline it is SKIPPED, not degraded — matching the offline branch,
288
+ # which leaves degraded_components empty (#279 S7 W7). expiredSuppressions is
289
+ # date-derived and runs OFFLINE too, off the _command_as_of() clock seam.
290
+ stale_suppressions: list = []
291
+ expired_suppressions = _lib_pricing_check.expired_allowlist_entries(
292
+ c.PRICING_DRIFT_ALLOWLIST, now_utc,
293
+ )
286
294
 
287
295
  if not args.offline:
288
296
  litellm, ok = _fetch_litellm_prices()
@@ -297,6 +305,10 @@ def cmd_pricing_check(args: argparse.Namespace) -> int:
297
305
  "missing_from_us": list(res.missing_from_us),
298
306
  "ahead_of_litellm": list(res.ahead_of_litellm),
299
307
  }
308
+ stale_suppressions = _lib_pricing_check.stale_allowlist_entries(
309
+ c.PRICING_DRIFT_ALLOWLIST, c.CLAUDE_MODEL_PRICING,
310
+ c.CODEX_MODEL_PRICING, scoped,
311
+ )
300
312
  else:
301
313
  status = "degraded"
302
314
  degraded.append("litellm")
@@ -307,11 +319,15 @@ def cmd_pricing_check(args: argparse.Namespace) -> int:
307
319
 
308
320
  # Actionable = any finding on a leg that ran. `ahead_of_litellm` is
309
321
  # NEVER actionable (invariant #2). A degraded leg contributes no finding.
322
+ # A stale OR expired suppression is actionable (remove the allowlist entry /
323
+ # re-sync the snapshot) — both wired in explicitly (#279 S7 W7).
310
324
  actionable = (
311
325
  bool(coverage)
312
326
  or bool(drift["value_drift"])
313
327
  or bool(drift["missing_from_us"])
314
328
  or bool(existence["unpriced_vendor_models"])
329
+ or bool(stale_suppressions)
330
+ or bool(expired_suppressions)
315
331
  )
316
332
 
317
333
  payload = {
@@ -322,6 +338,12 @@ def cmd_pricing_check(args: argparse.Namespace) -> int:
322
338
  "coverage": [dataclasses.asdict(g) for g in coverage],
323
339
  "drift": drift,
324
340
  "existence": existence,
341
+ # Additive keys on the existing schemaVersion 1 envelope (no bump — CLI
342
+ # contract additive rule). staleSuppressions: allowlist entries whose
343
+ # divergence no longer exists (network). expiredSuppressions: allowlist
344
+ # entries past their `expires` cutover (date).
345
+ "staleSuppressions": list(stale_suppressions),
346
+ "expiredSuppressions": list(expired_suppressions),
325
347
  "litellmSource": c.LITELLM_PRICES_URL,
326
348
  }
327
349
 
@@ -385,6 +407,23 @@ def _render_pricing_check_text(payload: dict, *, offline: bool, actionable: bool
385
407
  out("\n Existence: all vendor models priced.\n")
386
408
  elif ex["status"] == "degraded":
387
409
  out("\n Existence: /v1/models unavailable (skipped).\n")
410
+ stale = payload.get("staleSuppressions") or []
411
+ if stale:
412
+ out(f"\n Stale suppressions ({len(stale)}) — the divergence no "
413
+ f"longer exists, remove the allowlist entry:\n")
414
+ for e in stale:
415
+ fld = f".{e['field']}" if e.get("field") else ""
416
+ out(f" • {e['model']}{fld}\n")
417
+
418
+ # Expired suppressions are date-derived, so they render in BOTH offline and
419
+ # online modes.
420
+ expired = payload.get("expiredSuppressions") or []
421
+ if expired:
422
+ out(f"\n Expired suppressions ({len(expired)}) — past their `expires` "
423
+ f"cutover, remove the allowlist entry / re-sync the snapshot:\n")
424
+ for e in expired:
425
+ fld = f".{e['field']}" if e.get("field") else ""
426
+ out(f" • {e['model']}{fld} (expired {e.get('expires')})\n")
388
427
 
389
428
  # Single-sourced from cmd_pricing_check's exit-code predicate (don't
390
429
  # recompute the four-clause boolean here — it would drift).
@@ -28,7 +28,7 @@ import sys
28
28
 
29
29
  from _cctally_core import (
30
30
  _command_as_of,
31
- _reset_aware_floor,
31
+ _floored_week_max,
32
32
  eprint,
33
33
  open_db,
34
34
  parse_iso_datetime,
@@ -82,45 +82,18 @@ def _load_week_snapshots(
82
82
  "AND datetime(week_end_at) > datetime(?)",
83
83
  (until.isoformat(), since.isoformat()),
84
84
  )
85
- rows = cur.fetchall()
86
- # Resolve each week's clamp floor once (keyed on week_start_date) so a
87
- # mid-week credit doesn't re-inflate the per-project Used %. None means
88
- # "no floor" — every captured row counts.
89
- floor_epoch: dict[str, int | None] = {}
90
-
91
- def _floor_for(wsd, ws_iso, we_iso):
92
- if wsd not in floor_epoch:
93
- f_iso = _reset_aware_floor(conn, wsd, ws_iso, we_iso)
94
- floor_epoch[wsd] = (
95
- int(parse_iso_datetime(f_iso, "project.floor").timestamp())
96
- if f_iso else None
97
- )
98
- return floor_epoch[wsd]
99
-
100
- result: dict[dt.datetime, float] = {}
101
- for wsd, ws_iso, we_iso, cap_iso, pct in rows:
85
+ rows_in = []
86
+ for wsd, ws_iso, we_iso, cap_iso, pct in cur.fetchall():
102
87
  if ws_iso is None or pct is None:
103
88
  continue
104
- floor = _floor_for(wsd, ws_iso, we_iso)
105
- if floor is not None and cap_iso is not None:
106
- try:
107
- cap_epoch = int(
108
- parse_iso_datetime(str(cap_iso), "project.cap").timestamp()
109
- )
110
- except ValueError:
111
- cap_epoch = None
112
- # Drop pre-floor (stale pre-credit) snapshots from the MAX.
113
- if cap_epoch is not None and cap_epoch < floor:
114
- continue
115
- ws = dt.datetime.fromisoformat(
116
- str(ws_iso).replace("Z", "+00:00")
117
- )
89
+ ws = dt.datetime.fromisoformat(str(ws_iso).replace("Z", "+00:00"))
118
90
  key = ws.astimezone(dt.timezone.utc)
119
- pct_f = float(pct)
120
- prev = result.get(key)
121
- if prev is None or pct_f > prev:
122
- result[key] = pct_f
123
- return result
91
+ rows_in.append((key, wsd, ws_iso, we_iso, cap_iso, pct))
92
+ # Reset-aware per-week max (#290): the shared reducer resolves each
93
+ # week's clamp floor once (keyed on week_start_date) and drops stale
94
+ # pre-credit captures before taking the per-week maximum. The query
95
+ # above already filters NULL bounds, so canonical bounds are present.
96
+ return _floored_week_max(conn, rows_in)
124
97
  finally:
125
98
  conn.close()
126
99
 
@@ -352,7 +325,7 @@ def _project_json_output(
352
325
  "projects": projects_json,
353
326
  "warnings": warnings,
354
327
  }
355
- return json.dumps(payload, indent=2)
328
+ return json.dumps(_cctally().stamp_schema_version(payload), indent=2)
356
329
 
357
330
 
358
331
  def _project_sort_key(row: dict, sort_by: str, order: str):
@@ -390,10 +363,10 @@ def cmd_project(args: argparse.Namespace) -> int:
390
363
  # Flag-combination validation (must run before any expensive work).
391
364
  if args.weeks is not None and args.weeks < 1:
392
365
  eprint("Error: --weeks must be >= 1")
393
- return 1
366
+ return 2
394
367
  if args.weeks is not None and (args.since or args.until):
395
368
  eprint("Error: --weeks cannot be combined with --since/--until")
396
- return 1
369
+ return 2
397
370
  if args.since and args.until:
398
371
  # Parse both as dates using the same multi-format helper shape used
399
372
  # elsewhere in the codebase so YYYY-MM-DD and YYYYMMDD both compare
@@ -415,7 +388,7 @@ def cmd_project(args: argparse.Namespace) -> int:
415
388
  # complaint triggered by garbage input).
416
389
  if since_parsed is not None and until_parsed is not None and since_parsed > until_parsed:
417
390
  eprint("Error: --since must be <= --until")
418
- return 1
391
+ return 2
419
392
 
420
393
  now = _command_as_of()
421
394
  conn = open_db()
@@ -424,7 +397,10 @@ def cmd_project(args: argparse.Namespace) -> int:
424
397
  if args.since or args.until:
425
398
  parsed = c._parse_cli_date_range(args, now_utc=now)
426
399
  if isinstance(parsed, int):
427
- return parsed
400
+ # Translate the ccusage-parity helper's exit 1 into project's own
401
+ # native usage code 2 — project is cctally-native, not a ccusage
402
+ # drop-in (docs/cli-contract.md; #279 S6 W2).
403
+ return 2
428
404
  since_dt, until_dt = parsed
429
405
  since_dt = since_dt.astimezone(dt.timezone.utc)
430
406
  until_dt = until_dt.astimezone(dt.timezone.utc)