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
@@ -966,16 +966,24 @@ def _build_forecast_share_panel_data(options: dict,
966
966
  Reuses ``DataSnapshot.forecast`` (ForecastOutput) and, when populated
967
967
  by the sync thread, ``DataSnapshot.forecast_view`` (the kernel
968
968
  wrapper from issue #57) for the (100, 90) budget pair.
969
- ``projection_curve`` is synthesized from ``r_avg`` / ``r_recent`` /
970
- ``inputs.p_now`` — the same arithmetic ``snapshot_to_envelope`` does
971
- for ``week_avg_projection_pct`` / ``recent_24h_projection_pct``,
972
- extended across the next 7 days.
969
+
970
+ ``projected_end_pct`` is the kernel's SELECTED projection (#661 S2 spec
971
+ section 3.3) rather than a sixth re-derivation of it, and the curve and
972
+ the ceiling distances are measured from the same ceiling-corrected base
973
+ the kernel used. This function previously paired a RAW ``inputs.p_now``
974
+ with the corrected ``r_avg`` the kernel publishes and described that as
975
+ "the same arithmetic ``snapshot_to_envelope`` does", which stopped being
976
+ true the moment the correction landed.
977
+
978
+ Every one of those quantities is withheld at a right-censored reading and
979
+ on a week with no observed usage, because each is derived from a meter
980
+ that supplies no point estimate.
973
981
  """
974
982
  fc = getattr(snap, "forecast", None) if snap else None
975
983
  fc_view = getattr(snap, "forecast_view", None) if snap else None
976
984
  if fc is None:
977
985
  return {
978
- "projected_end_pct": 0.0,
986
+ "projected_end_pct": None,
979
987
  # With no forecast at all there is no rate, so neither ceiling has
980
988
  # a distance — the same withholding the populated path performs.
981
989
  "days_to_100pct": None,
@@ -987,25 +995,43 @@ def _build_forecast_share_panel_data(options: dict,
987
995
  "projection_curve": [],
988
996
  "confidence": "LOW CONF",
989
997
  }
998
+ from _lib_forecast import projection_base
999
+
990
1000
  inputs = getattr(fc, "inputs", None)
991
- p_now = float(getattr(inputs, "p_now", 0.0) or 0.0) if inputs else 0.0
1001
+ # The CEILING-CORRECTED base, through the one named resolver. `None` at a
1002
+ # right-censored reading, where there is no point estimate at all.
1003
+ base = projection_base(inputs) if inputs is not None else None
1004
+ p_now = None if base is None else float(base)
992
1005
  remaining_hours = float(
993
1006
  getattr(inputs, "remaining_hours", 0.0) or 0.0
994
1007
  ) if inputs else 0.0
995
1008
  confidence = getattr(inputs, "confidence", "ok") if inputs else "ok"
996
- r_avg = float(getattr(fc, "r_avg", 0.0) or 0.0)
1009
+ r_avg_raw = getattr(fc, "r_avg", None)
1010
+ r_avg = None if r_avg_raw is None else float(r_avg_raw)
997
1011
  r_recent_raw = getattr(fc, "r_recent", None)
998
1012
  r_recent = float(r_recent_raw) if r_recent_raw is not None else r_avg
999
- # End-of-week projected %
1000
- projected_end_pct = (p_now + r_avg * remaining_hours) / 100.0
1013
+ # End-of-week projected % — the kernel's selected value, expressed as the
1014
+ # fraction this panel's contract carries. `None` when the kernel withheld
1015
+ # it, which the templates render as `n/a` rather than as `0.0%`.
1016
+ selected = getattr(fc, "week_avg_projection_pct", None)
1017
+ projected_end_pct = None if selected is None else float(selected) / 100.0
1001
1018
  # Days to ceilings (simple inverse: hours-to-target / 24).
1002
- # The two exit conditions are opposite facts and must not share a value.
1003
- # `p_now >= target_pct` means the target is already reached, and zero days
1004
- # to it is true. `r_avg <= 0` means no rate was observed, so the target is
1005
- # not reachable on any timeline this data describes — and in the no-usage
1006
- # state BOTH hold (`p_now == 0`, `r_avg == 0`), so the shared `0.0`
1007
- # rendered `Days->90% 0.0`, stating the opposite of the truth.
1019
+ # The three exit conditions are different facts and must not share a
1020
+ # value. `p_now >= target_pct` means the target is already reached, and
1021
+ # zero days to it is true. `r_avg <= 0` means no rate was observed, so the
1022
+ # target is not reachable on any timeline this data describes. A withheld
1023
+ # base or rate means the meter supplies no distance at all.
1024
+ #
1025
+ # #661 S2 §3.1 changed which of these fires in the no-usage state. Before
1026
+ # the ceiling correction both `p_now == 0` and `r_avg == 0` held there,
1027
+ # and the shared `0.0` rendered `Days->90% 0.0`, stating the opposite of
1028
+ # the truth. The corrected point of a displayed 0 is 0.25, so NEITHER
1029
+ # holds now: the distance is a real, finite, very large number. Bounding
1030
+ # it is a PRESENTATION decision and is made in `_optional_days`, so this
1031
+ # function keeps publishing the exact figure.
1008
1032
  def _days_to_ceiling(target_pct: float) -> "float | None":
1033
+ if p_now is None or r_avg is None:
1034
+ return None
1009
1035
  if p_now >= target_pct:
1010
1036
  return 0.0
1011
1037
  if r_avg <= 0:
@@ -1045,18 +1071,23 @@ def _build_forecast_share_panel_data(options: dict,
1045
1071
  # defect one layer below the fix.
1046
1072
  _raw_dpp = getattr(inputs, "dollars_per_percent", None) if inputs else None
1047
1073
  dpp = None if _raw_dpp is None else float(_raw_dpp)
1048
- budgets["avg"] = None if dpp is None else dpp * r_avg * 24.0
1049
- budgets["recent_24h"] = None if dpp is None else dpp * r_recent * 24.0
1050
- # Projection curve — 7-day forward, using r_avg
1074
+ budgets["avg"] = (None if dpp is None or r_avg is None
1075
+ else dpp * r_avg * 24.0)
1076
+ budgets["recent_24h"] = (None if dpp is None or r_recent is None
1077
+ else dpp * r_recent * 24.0)
1078
+ # Projection curve — 7-day forward, using r_avg from the corrected base.
1079
+ # Empty when either is withheld: a curve drawn from a censored reading is
1080
+ # the same fabrication the scalar above refuses.
1051
1081
  today = _share_now_utc().date()
1052
1082
  projection_curve: list[dict] = []
1053
- for i in range(7):
1054
- d = today + dt.timedelta(days=i)
1055
- pct = (p_now + r_avg * (i * 24.0)) / 100.0
1056
- projection_curve.append({
1057
- "date": d.isoformat(),
1058
- "projected_pct_used": pct,
1059
- })
1083
+ if p_now is not None and r_avg is not None:
1084
+ for i in range(7):
1085
+ d = today + dt.timedelta(days=i)
1086
+ pct = (p_now + r_avg * (i * 24.0)) / 100.0
1087
+ projection_curve.append({
1088
+ "date": d.isoformat(),
1089
+ "projected_pct_used": pct,
1090
+ })
1060
1091
  return {
1061
1092
  "projected_end_pct": projected_end_pct,
1062
1093
  "days_to_100pct": days_to_100,