cctally 1.115.0 → 1.116.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 (55) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/bin/_cctally_cache.py +85 -4
  3. package/bin/_cctally_completion.py +80 -12
  4. package/bin/_cctally_core.py +106 -9
  5. package/bin/_cctally_dashboard.py +6 -9
  6. package/bin/_cctally_dashboard_envelope.py +26 -32
  7. package/bin/_cctally_db.py +85 -4
  8. package/bin/_cctally_doctor.py +38 -5
  9. package/bin/_cctally_five_hour.py +83 -1017
  10. package/bin/_cctally_journal.py +1307 -180
  11. package/bin/_cctally_journal_repair.py +9 -7
  12. package/bin/_cctally_oauth_samples.py +280 -0
  13. package/bin/_cctally_parser.py +5 -0
  14. package/bin/_cctally_quarantine.py +14 -4
  15. package/bin/_cctally_quota.py +70 -8
  16. package/bin/_cctally_quota_model.py +35 -4
  17. package/bin/_cctally_record.py +414 -123
  18. package/bin/_cctally_rederive.py +632 -235
  19. package/bin/_cctally_refresh.py +29 -0
  20. package/bin/_cctally_statusline.py +984 -47
  21. package/bin/_cctally_store.py +972 -58
  22. package/bin/_cctally_tui.py +62 -7
  23. package/bin/_cctally_update.py +25 -0
  24. package/bin/_lib_account_protocol.py +201 -18
  25. package/bin/_lib_clone_copy.py +46 -11
  26. package/bin/_lib_dashboard_sources.py +57 -15
  27. package/bin/_lib_detector_access_registry.py +273 -52
  28. package/bin/_lib_doctor.py +302 -1
  29. package/bin/_lib_journal.py +145 -25
  30. package/bin/_lib_journal_router.py +166 -48
  31. package/bin/_lib_oauth_samples.py +238 -0
  32. package/bin/_lib_pricing_check.py +0 -12
  33. package/bin/_lib_provider_activity.py +770 -0
  34. package/bin/_lib_quarantine.py +3 -2
  35. package/bin/_lib_quota_model.py +96 -6
  36. package/bin/_lib_rebuild_progress.py +146 -0
  37. package/bin/_lib_record.py +22 -0
  38. package/bin/_lib_rederive.py +299 -3
  39. package/bin/_lib_snapshot_cache.py +43 -1
  40. package/bin/_lib_stale_proof.py +708 -0
  41. package/bin/_lib_statusline_candidates.py +65 -5
  42. package/bin/_lib_worker_launches.py +315 -0
  43. package/bin/cctally +6 -11
  44. package/dashboard/static/assets/{ConversationsView-Bc8LCOAZ.js → ConversationsView-Dflb_myA.js} +1 -1
  45. package/dashboard/static/assets/DoctorModal-DFX5CfIs.js +1 -0
  46. package/dashboard/static/assets/{ModalRoot-DLPgyAM_.js → ModalRoot-HlxoagIa.js} +1 -1
  47. package/dashboard/static/assets/{ProjectsDrillPanel-z53a4v4M.js → ProjectsDrillPanel-C3PJy1xx.js} +1 -1
  48. package/dashboard/static/assets/{SourceDetailModal-Bw0T8W-W.js → SourceDetailModal-BczsWKhD.js} +1 -1
  49. package/dashboard/static/assets/{UpdateModal-D5AKx4Yw.js → UpdateModal-Os-Lo5cs.js} +1 -1
  50. package/dashboard/static/assets/{index-BXDOzSng.js → index-CYZ3NLqF.js} +3 -3
  51. package/dashboard/static/assets/{outlineNavigation-JObY7cUY.js → outlineNavigation-k-zIif6W.js} +1 -1
  52. package/dashboard/static/dashboard.html +1 -1
  53. package/package.json +7 -2
  54. package/bin/_lib_merge_gate.py +0 -452
  55. package/dashboard/static/assets/DoctorModal-D--qVBwe.js +0 -1
@@ -155,12 +155,37 @@ class PrefixHashAccumulator:
155
155
 
156
156
  The framing is `journal_prefix_hash`'s, verbatim: per segment, the 4-byte
157
157
  big-endian name length, the name, the 8-byte big-endian data length, then
158
- the data. Completed segments are absorbed into a running `sha256`; only the
159
- segment currently being streamed is buffered, so residency is bounded by one
160
- segment. At a segment transition the caller passes the boundary the next
161
- record's `prior_high_water` will name, which is the ONE offset in the
162
- outgoing segment that can still be asked for; its digest is precomputed
163
- before the buffer is released.
158
+ the data. The data length precedes the data, so `begin_segment` takes the
159
+ exact byte count the pass will feed (`size`) and frames the segment header
160
+ with it; every chunk is then hashed as it arrives and NO segment bytes are
161
+ retained (#997 D12). The buffered form held the whole current segment in a
162
+ `bytearray` grown by `extend` — 410 MB on the maintainer's largest segment
163
+ — and the freed growth steps stayed resident in the macOS allocator.
164
+
165
+ A digest at a fully fed segment end (every transition, and every boundary
166
+ not short of a torn tail) comes from the streamed state. A digest at any
167
+ other offset — an evidence point inside the current segment, a boundary
168
+ short of a torn trailing line, a feed that ended short of its declared
169
+ size — re-reads the segment through `open_segment(name)` in 1 MiB chunks,
170
+ which the production callers bind to the journal's one segment-read seam,
171
+ so an evidence re-read is visible to it.
172
+
173
+ Consumed extent (#997 D12, revision 10). Per segment the accumulator also
174
+ tracks the bytes actually fed (`consumed`) and a raw SHA-256 of those bytes
175
+ alone (the witness). An offset beyond `consumed` raises. Every re-read reads
176
+ through `consumed`, not only through the requested offset: one pass frames
177
+ the prefix digest at the offset and hashes `[0, consumed)` raw, and the
178
+ digest is returned only when the read was not short and that raw hash
179
+ equals the witness. A torn tail an appender truncated and replaced after
180
+ the pass read it, or a line the reader assembled from old and replacement
181
+ bytes across two reads, can therefore never be certified. A registered
182
+ boundary keeps its segment's `consumed` and witness with its saved hash
183
+ state. A segment fed short of its declared size is re-framed from its
184
+ consumed bytes through the same verified re-read at the next
185
+ `begin_segment`; when that re-read is unavailable or fails verification
186
+ the accumulator is poisoned and every later digest raises. A missing
187
+ opener, a short re-read or a failed verification raises
188
+ `PrefixEvidenceUnavailable`.
164
189
  """
165
190
 
166
191
  # A transition can only ever register one boundary, and consecutive empty
@@ -176,10 +201,28 @@ class PrefixHashAccumulator:
176
201
  # entry that is still live.
177
202
  _MAX_BOUNDARIES = 16
178
203
 
179
- def __init__(self) -> None:
204
+ #: Chunk size of an evidence re-read through the opener.
205
+ _REREAD_CHUNK = 1 << 20
206
+
207
+ def __init__(self, *, open_segment=None) -> None:
208
+ self._open_segment = open_segment
209
+ #: The hash of every segment before the current one, absorbed.
180
210
  self._running = hashlib.sha256()
181
211
  self._current_name = None
182
- self._current = bytearray()
212
+ #: `_running` plus the current segment's header (framed with its
213
+ #: declared size) and every byte fed so far.
214
+ self._segment = None
215
+ self._declared = 0
216
+ #: Bytes of the current segment actually fed (its consumed extent),
217
+ #: and the raw SHA-256 of exactly those bytes (the witness).
218
+ self._fed = 0
219
+ self._witness = hashlib.sha256()
220
+ #: Set when a short segment could not be re-framed from verified
221
+ #: bytes: the running state no longer describes the journal.
222
+ self._poisoned = None
223
+ #: `(segment, offset) -> (state before that segment, final digest or
224
+ #: None, consumed, witness digest)`; a `None` final digest is re-read
225
+ #: through the opener, verified against the witness, when read.
183
226
  self._boundaries: dict = {}
184
227
  self._boundary_order: list = []
185
228
  #: Bytes fed into an evidence digest, reported as the `protocol_evidence`
@@ -189,55 +232,92 @@ class PrefixHashAccumulator:
189
232
 
190
233
  # -- feeding ----------------------------------------------------------
191
234
 
192
- def begin_segment(self, name: str, boundary=None) -> None:
193
- """Start `name`, absorbing whatever segment was open before it.
235
+ def begin_segment(self, name: str, boundary=None, *, size: int) -> None:
236
+ """Start `name`, which the pass will feed `size` bytes of, absorbing
237
+ whatever segment was open before it.
194
238
 
195
239
  `boundary` is the `(segment, offset)` the next record's evidence would
196
- name — i.e. the streaming loop's current `prior_high_water`. It is
197
- resolved and cached HERE because the outgoing segment's bytes are gone
198
- immediately afterwards.
240
+ name — i.e. the streaming loop's current `prior_high_water`. Its hash
241
+ state is kept HERE because the outgoing segment's state is folded into
242
+ the running hash immediately afterwards.
199
243
  """
200
244
  if boundary is not None:
201
245
  self._register_boundary(boundary)
202
246
  if self._current_name is not None:
203
- # `hashlib.update` accepts the buffer directly, so the outgoing
204
- # segment is framed WITHOUT a copy of itself. The maintainer's
205
- # largest segment is 410 MB, and copying it here briefly doubled
206
- # that at every transition.
207
- with memoryview(self._current) as data:
208
- self._absorb(self._current_name, data)
247
+ self._running = self._completed_segment()
209
248
  self._current_name = name
210
- self._current = bytearray()
211
-
212
- def extend(self, data: bytes) -> None:
213
- """Absorb raw bytes read from the segment currently open."""
214
- self._current.extend(data)
249
+ self._declared = int(size)
250
+ self._fed = 0
251
+ self._witness = hashlib.sha256()
252
+ self._segment = self._running.copy()
253
+ self._header(self._segment, name, self._declared)
254
+
255
+ def extend(self, data) -> None:
256
+ """Hash raw bytes read from the segment currently open."""
257
+ self._segment.update(data)
258
+ self._witness.update(data)
259
+ self._fed += len(data)
215
260
 
216
261
  # -- reading ----------------------------------------------------------
217
262
 
218
263
  def digest_at(self, high_water) -> "str | None":
219
- """`journal_prefix_hash(high_water)`, from the streamed bytes."""
264
+ """`journal_prefix_hash(high_water)`, from the streamed state."""
220
265
  if high_water is None:
221
266
  return None
222
267
  segment, offset = high_water
223
- if segment == self._current_name:
224
- digest = self._running.copy()
225
- with memoryview(self._current)[:offset] as data:
226
- self._frame(digest, segment, data)
227
- self.bytes_hashed += len(data)
228
- self.digests_computed += 1
229
- return "sha256:" + digest.hexdigest()
230
- cached = self._boundaries.get((segment, offset))
231
- if cached is None:
268
+ if self._poisoned is not None:
232
269
  raise PrefixEvidenceUnavailable(
233
- f"no streamed prefix evidence for {segment}@{offset}"
270
+ f"no verified prefix evidence for {segment}@{offset}: "
271
+ f"{self._poisoned}"
234
272
  )
235
- self.bytes_hashed += cached[1]
273
+ if segment == self._current_name:
274
+ if self._streamed_end(offset):
275
+ digest = self._segment.copy()
276
+ else:
277
+ digest = self._verified_rehash(
278
+ self._running, segment, offset, self._fed,
279
+ self._witness.digest())
280
+ else:
281
+ cached = self._boundaries.get((segment, offset))
282
+ if cached is None:
283
+ raise PrefixEvidenceUnavailable(
284
+ f"no streamed prefix evidence for {segment}@{offset}"
285
+ )
286
+ before, final, consumed, witness = cached
287
+ digest = (final.copy() if final is not None
288
+ else self._verified_rehash(
289
+ before, segment, offset, consumed, witness))
290
+ self.bytes_hashed += offset
236
291
  self.digests_computed += 1
237
- return cached[0]
292
+ return "sha256:" + digest.hexdigest()
238
293
 
239
294
  # -- internals --------------------------------------------------------
240
295
 
296
+ def _streamed_end(self, offset) -> bool:
297
+ """Whether the streamed state IS the digest at `offset`: the current
298
+ segment was fed exactly its declared size and `offset` is its end."""
299
+ return offset == self._fed == self._declared
300
+
301
+ def _completed_segment(self):
302
+ """The running hash with the current segment absorbed as fed."""
303
+ if self._fed == self._declared or self._poisoned is not None:
304
+ return self._segment
305
+ # The feed ended short of the declared size (the segment shrank under
306
+ # the pass): frame the bytes actually fed, as the buffered form did,
307
+ # from a re-read verified against them. Bytes that cannot be verified
308
+ # leave no running state that describes the journal, so every later
309
+ # digest raises rather than certifying one.
310
+ try:
311
+ return self._verified_rehash(
312
+ self._running, self._current_name, self._fed, self._fed,
313
+ self._witness.digest())
314
+ except (PrefixEvidenceUnavailable, OSError) as exc:
315
+ self._poisoned = (
316
+ f"short segment {self._current_name} could not be re-framed "
317
+ f"from its consumed bytes ({exc})"
318
+ )
319
+ return self._segment
320
+
241
321
  def _register_boundary(self, boundary) -> None:
242
322
  segment, offset = boundary
243
323
  if segment != self._current_name:
@@ -247,23 +327,61 @@ class PrefixHashAccumulator:
247
327
  key = (segment, offset)
248
328
  if key in self._boundaries:
249
329
  return
250
- digest = self._running.copy()
251
- with memoryview(self._current)[:offset] as data:
252
- self._frame(digest, segment, data)
253
- size = len(data)
254
- self._boundaries[key] = ("sha256:" + digest.hexdigest(), size)
330
+ final = (self._segment.copy() if self._streamed_end(offset)
331
+ else None)
332
+ self._boundaries[key] = (self._running.copy(), final, self._fed,
333
+ self._witness.digest())
255
334
  self._boundary_order.append(key)
256
335
  while len(self._boundary_order) > self._MAX_BOUNDARIES:
257
336
  self._boundaries.pop(self._boundary_order.pop(0), None)
258
337
 
259
- def _absorb(self, name, data) -> None:
260
- self._frame(self._running, name, data)
338
+ def _verified_rehash(self, before, name: str, offset: int,
339
+ consumed: int, witness: bytes):
340
+ """`before` plus `name`'s first `offset` bytes, re-read from the
341
+ segment through the opener in `_REREAD_CHUNK` pieces.
342
+
343
+ The read always runs through `consumed`, the bytes the pass fed, and
344
+ its raw SHA-256 must equal `witness`; only then are the bytes the
345
+ digest frames the bytes the pass read."""
346
+ if offset > consumed:
347
+ raise PrefixEvidenceUnavailable(
348
+ f"prefix evidence {name}@{offset} lies beyond the "
349
+ f"{consumed} bytes the pass consumed"
350
+ )
351
+ digest = before.copy()
352
+ self._header(digest, name, offset)
353
+ if consumed <= 0:
354
+ return digest
355
+ if self._open_segment is None:
356
+ raise PrefixEvidenceUnavailable(
357
+ f"no segment opener to re-read prefix evidence for "
358
+ f"{name}@{offset}"
359
+ )
360
+ raw = hashlib.sha256()
361
+ pos = 0
362
+ with self._open_segment(name) as handle:
363
+ while pos < consumed:
364
+ chunk = handle.read(min(self._REREAD_CHUNK, consumed - pos))
365
+ if not chunk:
366
+ raise PrefixEvidenceUnavailable(
367
+ f"short re-read of prefix evidence for {name}@{offset}"
368
+ )
369
+ raw.update(chunk)
370
+ if pos < offset:
371
+ with memoryview(chunk)[:offset - pos] as framed:
372
+ digest.update(framed)
373
+ pos += len(chunk)
374
+ if raw.digest() != witness:
375
+ raise PrefixEvidenceUnavailable(
376
+ f"segment {name} no longer holds the {consumed} bytes the "
377
+ f"pass consumed; prefix evidence {name}@{offset} is not "
378
+ f"certified"
379
+ )
380
+ return digest
261
381
 
262
382
  @staticmethod
263
- def _frame(digest, name: str, data) -> None:
264
- """`data` is any bytes-like buffer; `hashlib` consumes it in place."""
383
+ def _header(digest, name: str, length: int) -> None:
265
384
  encoded = name.encode("utf-8")
266
385
  digest.update(len(encoded).to_bytes(4, "big"))
267
386
  digest.update(encoded)
268
- digest.update(len(data).to_bytes(8, "big"))
269
- digest.update(data)
387
+ digest.update(int(length).to_bytes(8, "big"))
@@ -0,0 +1,238 @@
1
+ """Provider samples: the raw api observation as an OAuth sample (#797/#823/#840
2
+ spec 4.3, D8 and D9).
3
+
4
+ Pure-fn layer (no I/O, no sibling imports). Every OAuth fetch this binary
5
+ performs records ``q``, the wall-clock instant immediately before the request
6
+ starts, and publishes it as the additive raw api ``obs`` payload key
7
+ ``oauth_request_started_at`` (:data:`REQUEST_STARTED_KEY`). The provider
8
+ sampled the response at some instant at or after ``q`` (assumption A-Q), so a
9
+ sample whose ``q`` is after a contributor's provider-activity instant is a
10
+ reading the contributor could not have seen. This module turns one raw
11
+ payload into one :class:`OAuthSample` — the row shape of the stats table
12
+ ``claude_oauth_samples`` — and states window identity once.
13
+
14
+ Values are the RAW payload values, never the MAX-clamped stored snapshot
15
+ values: the weekly percent and its reset (nearest-hour normalized exactly as
16
+ the week-boundary code does), the five-hour percent and its raw reset, and the
17
+ authoritative five-hour window key. That key is the producer's frozen planned
18
+ key when the payload carries a valid one (an OAuth publication always does
19
+ when it carries five-hour fields), else the pure canonical floor; a planned
20
+ key that contradicts its own raw reset is conflicting identity and yields no
21
+ key, so it can never supply a five-hour proof.
22
+
23
+ Instants: ``requested_at_utc`` is stored in one canonical full-precision form
24
+ (``YYYY-MM-DDTHH:MM:SS.ffffffZ``), so lexicographic order is chronological and
25
+ a strict ``q > t`` comparison is exact. ``captured_at_utc`` is the payload's
26
+ capture stamp verbatim (seconds precision, truncated): for an integral reset
27
+ ``R``, ``captured < R`` holds exactly when the true completion instant is
28
+ before ``R``, so the truncation never weakens the reset bound.
29
+ """
30
+ from __future__ import annotations
31
+
32
+ import dataclasses
33
+ import datetime as dt
34
+ import math
35
+ import re
36
+
37
+ #: The raw api observation payload key carrying ``q``.
38
+ REQUEST_STARTED_KEY = "oauth_request_started_at"
39
+ #: The one source an OAuth publication records.
40
+ API_SOURCE = "api"
41
+ #: `_lib_five_hour._FIVE_HOUR_JITTER_FLOOR_SECONDS`, restated because this
42
+ #: module imports no sibling; `tests/test_oauth_sample_projection_797.py`
43
+ #: pins the equality.
44
+ FIVE_HOUR_JITTER_FLOOR_SECONDS = 600
45
+ #: The tolerance a planned five-hour key may sit from its raw reset — the
46
+ #: same bound `_pipeline_claude_usage` validates a planned key against.
47
+ PLANNED_KEY_TOLERANCE_SECONDS = FIVE_HOUR_JITTER_FLOOR_SECONDS * 3
48
+ #: The capture stamp is truncated to the second, so a genuine ``q`` can sit
49
+ #: up to one second after it.
50
+ _CAPTURE_TRUNCATION_SECONDS = 1
51
+
52
+ _REQUEST_STARTED = re.compile(
53
+ r"^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d{1,6})?Z$")
54
+ _CANONICAL_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ"
55
+ _SECONDS_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
56
+
57
+
58
+ @dataclasses.dataclass(frozen=True)
59
+ class OAuthSample:
60
+ """One provider sample: a row of ``claude_oauth_samples``."""
61
+
62
+ account_key: str
63
+ observation_id: str
64
+ requested_at_utc: str
65
+ captured_at_utc: str
66
+ weekly_percent: "float | None"
67
+ weekly_resets_at_utc: "str | None"
68
+ five_hour_percent: "float | None"
69
+ five_hour_resets_at_utc: "str | None"
70
+ five_hour_window_key: "int | None"
71
+
72
+ def row(self) -> tuple:
73
+ """The column values in table order."""
74
+ return tuple(getattr(self, f.name) for f in dataclasses.fields(self))
75
+
76
+
77
+ def format_request_started(instant: dt.datetime) -> str:
78
+ """``instant`` (tz-aware) in the carrier's canonical full-precision UTC
79
+ form. A naive datetime is a programming error and raises."""
80
+ if instant.tzinfo is None:
81
+ raise ValueError("the request-start instant must be timezone-aware")
82
+ return instant.astimezone(dt.timezone.utc).strftime(_CANONICAL_FORMAT)
83
+
84
+
85
+ def _parse_aware(value) -> "dt.datetime | None":
86
+ if not isinstance(value, str) or not value:
87
+ return None
88
+ try:
89
+ parsed = dt.datetime.fromisoformat(
90
+ value[:-1] + "+00:00" if value.endswith("Z") else value)
91
+ except ValueError:
92
+ return None
93
+ if parsed.tzinfo is None:
94
+ return None
95
+ return parsed.astimezone(dt.timezone.utc)
96
+
97
+
98
+ def canonical_instant(value) -> str:
99
+ """Any tz-aware ISO-8601 instant (or tz-aware datetime) in the canonical
100
+ full-precision form used for ``requested_at_utc``; raises ``ValueError``
101
+ on anything else. Callers compare their own instants against stored
102
+ ``q`` values through this form."""
103
+ if isinstance(value, dt.datetime):
104
+ return format_request_started(value)
105
+ parsed = _parse_aware(value)
106
+ if parsed is None:
107
+ raise ValueError(f"not a timezone-aware ISO-8601 instant: {value!r}")
108
+ return parsed.strftime(_CANONICAL_FORMAT)
109
+
110
+
111
+ def parse_request_started(value) -> "str | None":
112
+ """The canonical ``q`` of a raw payload value, or None when it is missing
113
+ or unparsable. Only the carrier's own shape is accepted (UTC with ``Z``,
114
+ up to microsecond precision); a bool, a number or a naive instant is not
115
+ a key."""
116
+ if not isinstance(value, str) or not _REQUEST_STARTED.match(value):
117
+ return None
118
+ parsed = _parse_aware(value)
119
+ return None if parsed is None else parsed.strftime(_CANONICAL_FORMAT)
120
+
121
+
122
+ def normalize_weekly_reset(resets_at_epoch: int) -> str:
123
+ """The weekly window identity of a raw ``resets_at`` epoch: rounded to the
124
+ nearest hour exactly as `_cctally_core._normalize_week_boundary_dt` does
125
+ (minutes 0..29 down, 30..59 up), as ``YYYY-MM-DDTHH:00:00Z``."""
126
+ value = dt.datetime.fromtimestamp(int(resets_at_epoch), tz=dt.timezone.utc)
127
+ normalized = value.replace(minute=0, second=0, microsecond=0)
128
+ if value.minute >= 30:
129
+ normalized += dt.timedelta(hours=1)
130
+ return normalized.strftime(_SECONDS_FORMAT)
131
+
132
+
133
+ def weekly_window(window) -> str:
134
+ """A weekly window argument (a raw or normalized reset as an epoch int or
135
+ a tz-aware ISO instant) as its normalized identity."""
136
+ if isinstance(window, bool):
137
+ raise ValueError("a weekly window is a reset instant, not a bool")
138
+ if isinstance(window, int):
139
+ return normalize_weekly_reset(window)
140
+ parsed = _parse_aware(window)
141
+ if parsed is None:
142
+ raise ValueError(f"not a weekly reset instant: {window!r}")
143
+ return normalize_weekly_reset(int(parsed.timestamp()))
144
+
145
+
146
+ def five_hour_window_key(resets_at_epoch: int, planned) -> "int | None":
147
+ """The authoritative five-hour window key: the producer's planned key
148
+ when it is an int within the planned-key tolerance of the raw reset,
149
+ None when a planned key is present but contradicts it, else the pure
150
+ canonical floor (`_canonical_5h_window_key` without an anchor)."""
151
+ if planned is not None:
152
+ if (type(planned) is not int
153
+ or abs(planned - resets_at_epoch)
154
+ > PLANNED_KEY_TOLERANCE_SECONDS):
155
+ return None
156
+ return planned
157
+ return resets_at_epoch - (resets_at_epoch % FIVE_HOUR_JITTER_FLOOR_SECONDS)
158
+
159
+
160
+ def _percent(value) -> "float | None":
161
+ if isinstance(value, bool) or not isinstance(value, (int, float)):
162
+ return None
163
+ value = float(value)
164
+ return value if math.isfinite(value) else None
165
+
166
+
167
+ def _bound_account(payload: dict) -> "str | None":
168
+ binding = payload.get("account_binding")
169
+ if not isinstance(binding, dict):
170
+ return None
171
+ key = binding.get("accountKey")
172
+ return key if isinstance(key, str) and key else None
173
+
174
+
175
+ def sample_from_observation(payload: dict, *, account_key: str,
176
+ observation_id: str,
177
+ captured_at_utc: str) -> "OAuthSample | None":
178
+ """The provider sample of one raw Claude usage observation payload, or
179
+ None: for a non-api source, a missing or unparsable request-start key, a
180
+ request instant after the capture, a missing or unparsable capture
181
+ instant, or no Claude account binding naming ``account_key``.
182
+
183
+ Each axis is independent: an unparsable weekly or five-hour field leaves
184
+ that axis's columns None (no identity, so no proof on that axis), and a
185
+ sample with neither axis is no sample."""
186
+ if not isinstance(payload, dict) or payload.get("source") != API_SOURCE:
187
+ return None
188
+ if not isinstance(account_key, str) or not account_key:
189
+ return None
190
+ if not isinstance(observation_id, str) or not observation_id:
191
+ return None
192
+ if _bound_account(payload) != account_key:
193
+ return None
194
+ requested = parse_request_started(payload.get(REQUEST_STARTED_KEY))
195
+ captured = _parse_aware(captured_at_utc)
196
+ if requested is None or captured is None:
197
+ return None
198
+ requested_dt = _parse_aware(requested)
199
+ if (requested_dt - captured).total_seconds() > _CAPTURE_TRUNCATION_SECONDS:
200
+ return None
201
+
202
+ weekly_percent = _percent(payload.get("weekly_percent"))
203
+ weekly_reset = None
204
+ resets_at = payload.get("resets_at")
205
+ if not isinstance(resets_at, bool):
206
+ try:
207
+ weekly_reset = normalize_weekly_reset(int(resets_at))
208
+ except (TypeError, ValueError, OverflowError, OSError):
209
+ weekly_reset = None
210
+ if weekly_reset is None:
211
+ weekly_percent = None
212
+
213
+ five_percent = _percent(payload.get("five_hour_percent"))
214
+ five_reset = None
215
+ five_key = None
216
+ five_reset_dt = _parse_aware(payload.get("five_hour_resets_at"))
217
+ if five_reset_dt is not None:
218
+ five_reset = five_reset_dt.strftime(_SECONDS_FORMAT)
219
+ five_key = five_hour_window_key(
220
+ int(five_reset_dt.timestamp()),
221
+ payload.get("five_hour_window_key"))
222
+ if five_percent is None:
223
+ five_reset = None
224
+ five_key = None
225
+
226
+ if weekly_percent is None and five_percent is None:
227
+ return None
228
+ return OAuthSample(
229
+ account_key=account_key,
230
+ observation_id=observation_id,
231
+ requested_at_utc=requested,
232
+ captured_at_utc=captured_at_utc,
233
+ weekly_percent=weekly_percent,
234
+ weekly_resets_at_utc=weekly_reset,
235
+ five_hour_percent=five_percent,
236
+ five_hour_resets_at_utc=five_reset,
237
+ five_hour_window_key=five_key,
238
+ )
@@ -232,15 +232,3 @@ def check_table_shapes(claude_tbl, codex_tbl, zero_sentinels) -> list:
232
232
  for model, body in codex_tbl.items():
233
233
  _check(model, body, _CODEX_REQUIRED, allow_zero=model in zero_sentinels)
234
234
  return problems
235
-
236
-
237
- def pricing_issue_action(findings_present: bool, existing_open: bool) -> str:
238
- """Decide the cron's GitHub-issue action. Pure; the YAML executes it.
239
-
240
- ``findings_present`` is ANY actionable finding the cron tracks — value
241
- drift, missing-from-us, OR (as of #279 S7 W7) a stale/expired allowlist
242
- suppression. The name generalizes the former ``drift_present``; the
243
- two-state machine is unchanged."""
244
- if findings_present:
245
- return "update" if existing_open else "create"
246
- return "close" if existing_open else "noop"