cctally 1.103.0 → 1.105.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 (66) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/README.md +6 -6
  3. package/bin/_cctally_alerts.py +104 -10
  4. package/bin/_cctally_cache.py +193 -71
  5. package/bin/_cctally_config.py +62 -2
  6. package/bin/_cctally_core.py +62 -1
  7. package/bin/_cctally_dashboard.py +694 -70
  8. package/bin/_cctally_dashboard_conversation.py +400 -6
  9. package/bin/_cctally_dashboard_envelope.py +336 -14
  10. package/bin/_cctally_dashboard_perf.py +93 -0
  11. package/bin/_cctally_dashboard_share.py +56 -25
  12. package/bin/_cctally_dashboard_sources.py +988 -94
  13. package/bin/_cctally_db.py +30 -3
  14. package/bin/_cctally_diagnosis_sources.py +699 -186
  15. package/bin/_cctally_doctor.py +77 -0
  16. package/bin/_cctally_forecast.py +932 -51
  17. package/bin/_cctally_journal.py +430 -24
  18. package/bin/_cctally_parser.py +66 -0
  19. package/bin/_cctally_project.py +535 -10
  20. package/bin/_cctally_quota.py +28 -0
  21. package/bin/_cctally_quota_calibration.py +146 -0
  22. package/bin/_cctally_quota_model.py +2142 -0
  23. package/bin/_cctally_record.py +204 -30
  24. package/bin/_cctally_share.py +16 -8
  25. package/bin/_cctally_statusline.py +34 -0
  26. package/bin/_cctally_tui.py +531 -115
  27. package/bin/_lib_codex_conversation_query.py +258 -12
  28. package/bin/_lib_codex_hooks.py +26 -0
  29. package/bin/_lib_conversation_query.py +145 -15
  30. package/bin/_lib_dashboard_json.py +105 -0
  31. package/bin/_lib_dashboard_settings_contract.py +2 -0
  32. package/bin/_lib_diagnosis.py +21 -2
  33. package/bin/_lib_doctor.py +215 -1
  34. package/bin/_lib_forecast.py +337 -43
  35. package/bin/_lib_ingest_frontier.py +889 -0
  36. package/bin/_lib_meter_rate_change.py +360 -0
  37. package/bin/_lib_perf.py +22 -0
  38. package/bin/_lib_pricing.py +30 -3
  39. package/bin/_lib_quota_calibration.py +311 -0
  40. package/bin/_lib_quota_copy.py +157 -0
  41. package/bin/_lib_quota_model.py +2520 -0
  42. package/bin/_lib_rate_change_delivery.py +119 -0
  43. package/bin/_lib_record.py +50 -0
  44. package/bin/_lib_rederive.py +10 -0
  45. package/bin/_lib_render.py +6 -0
  46. package/bin/_lib_retained_size.py +187 -0
  47. package/bin/_lib_share_templates.py +37 -5
  48. package/bin/_lib_snapshot_cache.py +504 -17
  49. package/bin/_lib_statusline.py +200 -2
  50. package/bin/_lib_tick_stats.py +28 -4
  51. package/bin/_lib_view_models.py +30 -12
  52. package/bin/cctally +32 -0
  53. package/dashboard/static/assets/ConversationsView-DA4j7Yso.js +72 -0
  54. package/dashboard/static/assets/DoctorModal-D4PbVnbE.js +1 -0
  55. package/dashboard/static/assets/ModalRoot-D4oSPxPp.js +1 -0
  56. package/dashboard/static/assets/ProjectsDrillPanel-DkPlB9JH.js +1 -0
  57. package/dashboard/static/assets/SourceDetailModal-BE7FONsS.js +1 -0
  58. package/dashboard/static/assets/UpdateModal-BJuI8glf.js +7 -0
  59. package/dashboard/static/assets/index-klO46NcU.css +1 -0
  60. package/dashboard/static/assets/index-w_bINkJ8.js +13 -0
  61. package/dashboard/static/assets/outlineNavigation-zDVm6Hdd.js +9 -0
  62. package/dashboard/static/assets/useKeymap-CJ-Pi17D.js +1 -0
  63. package/dashboard/static/dashboard.html +3 -2
  64. package/package.json +10 -1
  65. package/dashboard/static/assets/index-Di2hljvB.css +0 -1
  66. package/dashboard/static/assets/index-XYCIWjVG.js +0 -97
@@ -400,7 +400,17 @@ _init_paths_from_env()
400
400
  # removing the shared-memory WAL page map implicated by the retained corruption
401
401
  # bundles. The one-time rebuild is the mode transition; no stats migration is
402
402
  # added and cache/conversations remain WAL/NORMAL.
403
- STATS_INDEX_EPOCH = 1010
403
+ # 1010 -> 1011 (#661 S2 §6.4): the meter-rate-change event family. Adds
404
+ # `meter_rate_change_events`, a durable forward-only record that one provider
405
+ # metering-rate transition was observed, keyed
406
+ # `(provider, account_key, effective_from)`. It is an epoch bump and NOT a
407
+ # stats migration for the two reasons every bump since 1005 carries: the
408
+ # 13-entry registry is frozen, AND an epoch-current open returns before any
409
+ # schema work, so a `@stats_migration` handler would never run on an upgraded
410
+ # install and the table would simply never appear. `quota_alert_arming` is the
411
+ # precedent the family follows — a journaled STATE record whose boundary
412
+ # survives a rebuild, so history cannot re-fire.
413
+ STATS_INDEX_EPOCH = 1011
404
414
  LEGACY_STATS_HEAD = 13
405
415
 
406
416
  #: #496 S1 F1. A NEW branch, for a state that cannot occur before the
@@ -965,6 +975,12 @@ _ALERTS_CONFIG_VALID_KEYS = {
965
975
  "weekly_thresholds",
966
976
  "five_hour_thresholds",
967
977
  "projected_enabled",
978
+ # #661 S2 §6.2. The metering-rate-change family's PUSH toggle, and only
979
+ # its push toggle: events are recorded from the first upgrade whatever
980
+ # this says, so every pull surface shows the state with no configuration.
981
+ # It defaults OFF like every other alert toggle, so an upgrade never
982
+ # produces a surprise notification.
983
+ "rate_change_enabled",
968
984
  "notifier",
969
985
  "command_template",
970
986
  }
@@ -1062,6 +1078,17 @@ def _get_alerts_config(cfg: "dict | None") -> dict:
1062
1078
  f"{type(projected_enabled).__name__}: {projected_enabled!r}",
1063
1079
  field="alerts.projected_enabled",
1064
1080
  )
1081
+ # #661 S2 §6.2: the rate-change PUSH toggle, default OFF. Recording is
1082
+ # unconditional and does not consult this — the split is what lets a user
1083
+ # learn that the rate changed without opting in while nothing arrives
1084
+ # unbidden. Bool-validated rather than coerced, like `projected_enabled`.
1085
+ rate_change_enabled = block.get("rate_change_enabled", False)
1086
+ if not isinstance(rate_change_enabled, bool):
1087
+ raise _AlertsConfigError(
1088
+ f"alerts.rate_change_enabled must be a JSON boolean, got "
1089
+ f"{type(rate_change_enabled).__name__}: {rate_change_enabled!r}",
1090
+ field="alerts.rate_change_enabled",
1091
+ )
1065
1092
  # Dispatch-global keys (Phase B). `notifier` selects the backend;
1066
1093
  # `command_template` is an argv list for the `command` backend (and may be
1067
1094
  # set ahead of switching the backend). The cross-field constraint
@@ -1109,6 +1136,7 @@ def _get_alerts_config(cfg: "dict | None") -> dict:
1109
1136
  "weekly_thresholds": weekly,
1110
1137
  "five_hour_thresholds": five_hour,
1111
1138
  "projected_enabled": projected_enabled,
1139
+ "rate_change_enabled": rate_change_enabled,
1112
1140
  "notifier": notifier,
1113
1141
  "command_template": command_template,
1114
1142
  }
@@ -1637,6 +1665,39 @@ def _apply_quota_projection_schema(conn: sqlite3.Connection) -> None:
1637
1665
  PRIMARY KEY(source)
1638
1666
  );
1639
1667
 
1668
+ -- #661 S2 §6: a provider metering-rate transition, recorded from the
1669
+ -- first upgrade whether or not notification is enabled (§6.2). The
1670
+ -- row is the durable forward-only latch: its presence is what stops
1671
+ -- the same transition alerting twice, and it survives a stats.db
1672
+ -- rebuild because the journal event that created it carries a
1673
+ -- self-sufficient payload (§6.5).
1674
+ --
1675
+ -- The identity is `(provider, account_key, effective_from)` and the
1676
+ -- promise is stated at exactly that precision — once per exact key,
1677
+ -- not "once per provider regime". `reduce_state` closes and
1678
+ -- re-creates regimes on a fingerprint change and a revised detector
1679
+ -- can select a different `effective_from` for the same underlying
1680
+ -- transition; separately, R8 can turn a formerly merged identity into
1681
+ -- a real account key. Both produce a new key for a transition a human
1682
+ -- would call the same one. The FINGERPRINT stays out of the key
1683
+ -- deliberately: including it would re-alert on our own algorithm
1684
+ -- revisions.
1685
+ CREATE TABLE IF NOT EXISTS meter_rate_change_events (
1686
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1687
+ provider TEXT NOT NULL,
1688
+ account_key TEXT NOT NULL DEFAULT 'unattributed',
1689
+ effective_from TEXT NOT NULL,
1690
+ previous_units_per_point REAL NOT NULL,
1691
+ new_units_per_point REAL NOT NULL,
1692
+ severity TEXT NOT NULL,
1693
+ detected_at_utc TEXT NOT NULL,
1694
+ created_at_utc TEXT NOT NULL,
1695
+ notified_at TEXT,
1696
+ UNIQUE(provider, account_key, effective_from)
1697
+ );
1698
+ CREATE INDEX IF NOT EXISTS idx_meter_rate_change_events_key
1699
+ ON meter_rate_change_events(provider, account_key, effective_from);
1700
+
1640
1701
  CREATE TABLE IF NOT EXISTS quota_alert_arming (
1641
1702
  id INTEGER PRIMARY KEY AUTOINCREMENT,
1642
1703
  source TEXT NOT NULL,