cctally 1.113.0 → 1.115.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 (97) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +4 -6
  3. package/bin/_cctally_account.py +32 -8
  4. package/bin/_cctally_cache.py +6035 -722
  5. package/bin/_cctally_completion.py +1287 -0
  6. package/bin/_cctally_config.py +37 -0
  7. package/bin/_cctally_core.py +400 -2
  8. package/bin/_cctally_dashboard.py +98 -20
  9. package/bin/_cctally_dashboard_conversation.py +292 -23
  10. package/bin/_cctally_dashboard_envelope.py +16 -3
  11. package/bin/_cctally_dashboard_perf.py +128 -9
  12. package/bin/_cctally_dashboard_sources.py +285 -213
  13. package/bin/_cctally_db.py +1177 -23
  14. package/bin/_cctally_diagnosis_sources.py +22 -4
  15. package/bin/_cctally_doctor.py +572 -45
  16. package/bin/_cctally_five_hour.py +84 -33
  17. package/bin/_cctally_forecast.py +108 -15
  18. package/bin/_cctally_journal.py +3950 -116
  19. package/bin/_cctally_journal_repair.py +101 -12
  20. package/bin/_cctally_milestone_history.py +82 -9
  21. package/bin/_cctally_parser.py +42 -0
  22. package/bin/_cctally_percent_breakdown.py +3 -2
  23. package/bin/_cctally_project.py +8 -2
  24. package/bin/_cctally_quarantine.py +652 -0
  25. package/bin/_cctally_quota.py +227 -37
  26. package/bin/_cctally_quota_model.py +360 -83
  27. package/bin/_cctally_record.py +691 -149
  28. package/bin/_cctally_rederive.py +320 -41
  29. package/bin/_cctally_refresh.py +84 -13
  30. package/bin/_cctally_source_analytics.py +83 -17
  31. package/bin/_cctally_statusline.py +6154 -517
  32. package/bin/_cctally_store.py +367 -53
  33. package/bin/_cctally_transcript.py +8 -7
  34. package/bin/_cctally_tui.py +61 -56
  35. package/bin/_cctally_weekrefs.py +128 -21
  36. package/bin/_lib_account_binding.py +152 -0
  37. package/bin/_lib_account_protocol.py +386 -0
  38. package/bin/_lib_aggregators.py +4 -0
  39. package/bin/_lib_cache_coverage.py +13 -1
  40. package/bin/_lib_cache_report.py +35 -6
  41. package/bin/_lib_cache_writer_lock.py +19 -3
  42. package/bin/_lib_clone_copy.py +142 -0
  43. package/bin/_lib_codex_conversation.py +32 -0
  44. package/bin/_lib_codex_conversation_query.py +297 -49
  45. package/bin/_lib_codex_conversation_watch.py +59 -14
  46. package/bin/_lib_codex_drain_admission.py +214 -0
  47. package/bin/_lib_codex_metadata.py +18 -1
  48. package/bin/_lib_codex_publication_barrier.py +277 -0
  49. package/bin/_lib_codex_rollout_container.py +502 -0
  50. package/bin/_lib_conversation_dispatch.py +3 -0
  51. package/bin/_lib_conversation_query.py +106 -59
  52. package/bin/_lib_conversation_retention.py +1605 -640
  53. package/bin/_lib_cost_provenance.py +53 -2
  54. package/bin/_lib_detector_access_registry.py +3817 -0
  55. package/bin/_lib_doctor.py +714 -69
  56. package/bin/_lib_five_hour.py +126 -0
  57. package/bin/_lib_fmt.py +11 -0
  58. package/bin/_lib_ingest_frontier.py +414 -20
  59. package/bin/_lib_journal.py +141 -10
  60. package/bin/_lib_jsonl.py +5 -4
  61. package/bin/_lib_legacy_spool.py +126 -0
  62. package/bin/_lib_legacy_writer_evidence.py +136 -0
  63. package/bin/_lib_merge_gate.py +231 -86
  64. package/bin/_lib_meter_rate_change.py +10 -0
  65. package/bin/_lib_outcome_quarantine.py +690 -0
  66. package/bin/_lib_perf.py +27 -1
  67. package/bin/_lib_pricing.py +199 -9
  68. package/bin/_lib_pricing_check.py +20 -4
  69. package/bin/_lib_quarantine.py +154 -0
  70. package/bin/_lib_quota_calibration.py +53 -8
  71. package/bin/_lib_quota_copy.py +2 -1
  72. package/bin/_lib_quota_model.py +288 -46
  73. package/bin/_lib_reclaim_planner.py +792 -0
  74. package/bin/_lib_rederive.py +319 -82
  75. package/bin/_lib_segment_summary.py +35 -2
  76. package/bin/_lib_snapshot_cache.py +21 -0
  77. package/bin/_lib_sqlite_close.py +81 -0
  78. package/bin/_lib_statusline_candidates.py +292 -23
  79. package/bin/_lib_subscription_weeks.py +100 -17
  80. package/bin/_lib_tick_stats.py +217 -17
  81. package/bin/_lib_wal_checkpoint.py +1077 -0
  82. package/bin/_lib_write_budget.py +483 -0
  83. package/bin/_lib_write_io.py +703 -0
  84. package/bin/cctally +31 -1
  85. package/dashboard/static/assets/{ConversationsView-CEZ-BUyi.js → ConversationsView-Bc8LCOAZ.js} +1 -1
  86. package/dashboard/static/assets/{DoctorModal-CE1oH_Zy.js → DoctorModal-D--qVBwe.js} +1 -1
  87. package/dashboard/static/assets/ModalRoot-DLPgyAM_.js +1 -0
  88. package/dashboard/static/assets/{ProjectsDrillPanel-B3DZoCeD.js → ProjectsDrillPanel-z53a4v4M.js} +1 -1
  89. package/dashboard/static/assets/{SourceDetailModal-CHp9EgLP.js → SourceDetailModal-Bw0T8W-W.js} +1 -1
  90. package/dashboard/static/assets/{UpdateModal-5iKST6jo.js → UpdateModal-D5AKx4Yw.js} +1 -1
  91. package/dashboard/static/assets/index-BKITk1CT.css +1 -0
  92. package/dashboard/static/assets/{index-Cm0VTxJC.js → index-BXDOzSng.js} +3 -3
  93. package/dashboard/static/assets/{outlineNavigation-DkXGL8J6.js → outlineNavigation-JObY7cUY.js} +1 -1
  94. package/dashboard/static/dashboard.html +2 -2
  95. package/package.json +19 -1
  96. package/dashboard/static/assets/ModalRoot-D5Y8IFUG.js +0 -1
  97. package/dashboard/static/assets/index-Dt4tqf43.css +0 -1
@@ -11,6 +11,11 @@ drift independently; the regression `bin/cctally-5h-canonical-test`
11
11
  pins the cross-shape equivalence (epoch-int → datetime → epoch round-trip
12
12
  matches the modulo floor on 600-aligned base epochs).
13
13
 
14
+ `latest_five_hour_reading` is the one read-only selection of an account's
15
+ newest five-hour reading (#825/#832 spec 4.7 revision 7): it takes the
16
+ caller's connection and runs one bounded statement, so the module stays free
17
+ of I/O at import time and of sibling imports.
18
+
14
19
  `bin/cctally` re-exports every symbol below so internal call sites and
15
20
  SourceFileLoader-based tests/fixtures (`tests/test_blocks_recorded_anchor`,
16
21
  `tests/test_five_hour_block_selector`, `tests/test_five_hour_blocks_json`,
@@ -24,6 +29,7 @@ from __future__ import annotations
24
29
 
25
30
  import datetime as dt
26
31
  import math
32
+ from dataclasses import dataclass
27
33
 
28
34
 
29
35
  _FIVE_HOUR_JITTER_FLOOR_SECONDS = 600 # 10 minutes; tolerance band for resets_at jitter
@@ -141,3 +147,123 @@ def _canonical_5h_window_key(
141
147
  ):
142
148
  return prior_key
143
149
  return resets_at_epoch - (resets_at_epoch % _FIVE_HOUR_JITTER_FLOOR_SECONDS)
150
+
151
+
152
+ @dataclass(frozen=True)
153
+ class FiveHourReading:
154
+ """One five-hour reading, every field taken from ONE snapshot row.
155
+
156
+ ``resets_at`` is the row's stored ``five_hour_resets_at`` text (``None`` on
157
+ a hand-built or legacy row that stores only the canonical key) and
158
+ ``window_key`` its canonical window: the stored key, or the
159
+ ``_canonical_5h_window_key`` floor of the stored reset for a row that
160
+ predates the key. ``snapshot_id`` / ``captured_at_utc`` / ``account_key`` identify the
161
+ row, and ``weekly_observation_held`` says whether its WEEKLY axis was
162
+ carried forward — its five-hour fields are the writing tick's own either
163
+ way.
164
+ """
165
+
166
+ percent: float
167
+ resets_at: "str | None"
168
+ window_key: int
169
+ captured_at_utc: str
170
+ snapshot_id: int
171
+ account_key: str
172
+ weekly_observation_held: bool
173
+
174
+
175
+ def latest_five_hour_reading(
176
+ conn,
177
+ *,
178
+ account_key: "str | None" = None,
179
+ as_of_utc: "str | None" = None,
180
+ captured_at_or_after_utc: "str | None" = None,
181
+ week_start_date: "str | None" = None,
182
+ week_start_at_in: "tuple[str, ...] | list[str] | None" = None,
183
+ ) -> "FiveHourReading | None":
184
+ """The newest ``weekly_usage_snapshots`` row carrying a five-hour reading
185
+ (#825/#832 spec 4.7 revision 7 *Latest five-hour reading*).
186
+
187
+ The ONE shared, read-only selection every reader of the newest five-hour
188
+ value uses. A five-hour credit retires a stale replica in place (its three
189
+ five-hour columns NULL, its weekly evidence kept), and an ordinary
190
+ weekly-only reading has the same shape, so either can be the newest row
191
+ while saying nothing about the five-hour axis. Eligible rows carry a
192
+ finite, non-negative percent and a resolvable canonical window (the stored
193
+ ``five_hour_window_key``, else ``_canonical_5h_window_key`` of a parseable
194
+ ``five_hour_resets_at``; the recorder always writes all three together),
195
+ held rows included (a held row's five-hour fields are the writing tick's
196
+ own). Zero is a valid percent. Weekly evidence is NOT selected here: callers select it
197
+ independently, with their own freshness and held-row rules.
198
+
199
+ Every restriction is the CALLER's, applied unchanged:
200
+
201
+ - ``account_key``: ``None`` adds no account predicate (the caller's own
202
+ account-blind contract); a key, including ``unattributed``, scopes the
203
+ selection to that account, so one account never borrows another's
204
+ reading.
205
+ - ``as_of_utc`` / ``captured_at_or_after_utc``: an inclusive capture
206
+ window, compared as instants.
207
+ - ``week_start_date`` alone: one week date key. With ``week_start_at_in``:
208
+ the week whose ``week_start_at`` is one of those texts, or a
209
+ ``week_start_at``-less row of that date (the current-week reader's
210
+ scope).
211
+
212
+ The selection is one ``LIMIT 1`` statement, so no history-sized sort is
213
+ materialized. ``None`` means no eligible row.
214
+ """
215
+ clauses = [
216
+ "five_hour_percent IS NOT NULL",
217
+ "five_hour_percent >= 0",
218
+ "five_hour_percent <= 1e308",
219
+ "(five_hour_window_key IS NOT NULL "
220
+ " OR unixepoch(five_hour_resets_at) IS NOT NULL)",
221
+ ]
222
+ params: list = []
223
+ if account_key is not None:
224
+ clauses.append("account_key = ?")
225
+ params.append(account_key)
226
+ if as_of_utc is not None:
227
+ clauses.append("unixepoch(captured_at_utc) <= unixepoch(?)")
228
+ params.append(as_of_utc)
229
+ if captured_at_or_after_utc is not None:
230
+ clauses.append("unixepoch(captured_at_utc) >= unixepoch(?)")
231
+ params.append(captured_at_or_after_utc)
232
+ if week_start_at_in is not None:
233
+ texts = tuple(week_start_at_in)
234
+ if not texts:
235
+ clauses.append("(week_start_at IS NULL AND week_start_date = ?)")
236
+ else:
237
+ clauses.append(
238
+ "(week_start_at IN (" + ",".join("?" * len(texts)) + ") "
239
+ "OR (week_start_at IS NULL AND week_start_date = ?))")
240
+ params.extend(texts)
241
+ params.append(week_start_date)
242
+ elif week_start_date is not None:
243
+ clauses.append("week_start_date = ?")
244
+ params.append(week_start_date)
245
+ row = conn.execute(
246
+ "SELECT id, account_key, captured_at_utc, five_hour_percent, "
247
+ " five_hour_resets_at, five_hour_window_key, "
248
+ " unixepoch(five_hour_resets_at), weekly_observation_held "
249
+ "FROM weekly_usage_snapshots WHERE " + " AND ".join(clauses) +
250
+ " ORDER BY captured_at_utc DESC, id DESC LIMIT 1",
251
+ tuple(params),
252
+ ).fetchone()
253
+ if row is None:
254
+ return None
255
+ percent = float(row[3])
256
+ if not math.isfinite(percent) or percent < 0:
257
+ return None
258
+ window_key = (
259
+ int(row[5]) if row[5] is not None
260
+ else _canonical_5h_window_key(int(row[6])))
261
+ return FiveHourReading(
262
+ percent=percent,
263
+ resets_at=row[4],
264
+ window_key=window_key,
265
+ captured_at_utc=row[2],
266
+ snapshot_id=int(row[0]),
267
+ account_key=row[1],
268
+ weekly_observation_held=bool(row[7]),
269
+ )
package/bin/_lib_fmt.py CHANGED
@@ -292,6 +292,17 @@ def _fmt_num(n: int) -> str:
292
292
  return f"{n:,}"
293
293
 
294
294
 
295
+ def _fmt_usd_accounting(value: float, places: int) -> str:
296
+ """`-$71.21`: the sign goes before the currency symbol (#886).
297
+
298
+ The CLI convention shared with `_lib_diff_kernel` and `_lib_share`'s
299
+ MoneyCell: an ASCII hyphen, no `+` for a plain amount, and a sign only
300
+ when `value < 0` — so a negative zero prints unsigned.
301
+ """
302
+ sign = "-" if value < 0 else ""
303
+ return f"{sign}${abs(value):.{places}f}"
304
+
305
+
295
306
  def _truncate_num(formatted: str, width: int) -> str:
296
307
  """Truncate a formatted number to fit width, replacing tail with '…'."""
297
308
  if len(formatted) <= width: