cctally 1.114.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 (86) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/bin/_cctally_account.py +32 -8
  3. package/bin/_cctally_cache.py +4491 -295
  4. package/bin/_cctally_completion.py +1355 -0
  5. package/bin/_cctally_config.py +37 -0
  6. package/bin/_cctally_core.py +282 -9
  7. package/bin/_cctally_dashboard.py +8 -11
  8. package/bin/_cctally_dashboard_conversation.py +292 -23
  9. package/bin/_cctally_dashboard_envelope.py +42 -35
  10. package/bin/_cctally_dashboard_sources.py +18 -9
  11. package/bin/_cctally_db.py +401 -9
  12. package/bin/_cctally_doctor.py +469 -27
  13. package/bin/_cctally_five_hour.py +166 -1049
  14. package/bin/_cctally_forecast.py +81 -9
  15. package/bin/_cctally_journal.py +5076 -177
  16. package/bin/_cctally_journal_repair.py +110 -19
  17. package/bin/_cctally_oauth_samples.py +280 -0
  18. package/bin/_cctally_parser.py +47 -0
  19. package/bin/_cctally_project.py +8 -2
  20. package/bin/_cctally_quarantine.py +662 -0
  21. package/bin/_cctally_quota.py +96 -12
  22. package/bin/_cctally_quota_model.py +391 -83
  23. package/bin/_cctally_record.py +1088 -269
  24. package/bin/_cctally_rederive.py +854 -192
  25. package/bin/_cctally_refresh.py +113 -13
  26. package/bin/_cctally_source_analytics.py +23 -2
  27. package/bin/_cctally_statusline.py +7062 -505
  28. package/bin/_cctally_store.py +1244 -91
  29. package/bin/_cctally_transcript.py +8 -7
  30. package/bin/_cctally_tui.py +119 -60
  31. package/bin/_cctally_update.py +25 -0
  32. package/bin/_lib_account_binding.py +152 -0
  33. package/bin/_lib_account_protocol.py +569 -0
  34. package/bin/_lib_aggregators.py +4 -0
  35. package/bin/_lib_cache_coverage.py +13 -1
  36. package/bin/_lib_cache_writer_lock.py +19 -3
  37. package/bin/_lib_clone_copy.py +177 -0
  38. package/bin/_lib_codex_conversation_query.py +133 -19
  39. package/bin/_lib_codex_conversation_watch.py +59 -14
  40. package/bin/_lib_codex_drain_admission.py +214 -0
  41. package/bin/_lib_codex_metadata.py +18 -1
  42. package/bin/_lib_codex_publication_barrier.py +277 -0
  43. package/bin/_lib_codex_rollout_container.py +502 -0
  44. package/bin/_lib_conversation_dispatch.py +3 -0
  45. package/bin/_lib_conversation_query.py +11 -3
  46. package/bin/_lib_dashboard_sources.py +57 -15
  47. package/bin/_lib_detector_access_registry.py +4038 -0
  48. package/bin/_lib_doctor.py +753 -1
  49. package/bin/_lib_five_hour.py +126 -0
  50. package/bin/_lib_ingest_frontier.py +414 -20
  51. package/bin/_lib_journal.py +261 -10
  52. package/bin/_lib_journal_router.py +166 -48
  53. package/bin/_lib_jsonl.py +5 -4
  54. package/bin/_lib_legacy_spool.py +126 -0
  55. package/bin/_lib_legacy_writer_evidence.py +136 -0
  56. package/bin/_lib_meter_rate_change.py +10 -0
  57. package/bin/_lib_oauth_samples.py +238 -0
  58. package/bin/_lib_outcome_quarantine.py +690 -0
  59. package/bin/_lib_pricing.py +7 -2
  60. package/bin/_lib_pricing_check.py +0 -12
  61. package/bin/_lib_provider_activity.py +770 -0
  62. package/bin/_lib_quarantine.py +155 -0
  63. package/bin/_lib_quota_calibration.py +53 -8
  64. package/bin/_lib_quota_copy.py +2 -1
  65. package/bin/_lib_quota_model.py +384 -52
  66. package/bin/_lib_rebuild_progress.py +146 -0
  67. package/bin/_lib_record.py +22 -0
  68. package/bin/_lib_rederive.py +393 -8
  69. package/bin/_lib_segment_summary.py +35 -2
  70. package/bin/_lib_snapshot_cache.py +43 -1
  71. package/bin/_lib_stale_proof.py +708 -0
  72. package/bin/_lib_statusline_candidates.py +356 -27
  73. package/bin/_lib_worker_launches.py +315 -0
  74. package/bin/cctally +29 -12
  75. package/dashboard/static/assets/{ConversationsView-Bc8LCOAZ.js → ConversationsView-Dflb_myA.js} +1 -1
  76. package/dashboard/static/assets/DoctorModal-DFX5CfIs.js +1 -0
  77. package/dashboard/static/assets/{ModalRoot-DLPgyAM_.js → ModalRoot-HlxoagIa.js} +1 -1
  78. package/dashboard/static/assets/{ProjectsDrillPanel-z53a4v4M.js → ProjectsDrillPanel-C3PJy1xx.js} +1 -1
  79. package/dashboard/static/assets/{SourceDetailModal-Bw0T8W-W.js → SourceDetailModal-BczsWKhD.js} +1 -1
  80. package/dashboard/static/assets/{UpdateModal-D5AKx4Yw.js → UpdateModal-Os-Lo5cs.js} +1 -1
  81. package/dashboard/static/assets/{index-BXDOzSng.js → index-CYZ3NLqF.js} +3 -3
  82. package/dashboard/static/assets/{outlineNavigation-JObY7cUY.js → outlineNavigation-k-zIif6W.js} +1 -1
  83. package/dashboard/static/dashboard.html +1 -1
  84. package/package.json +20 -2
  85. package/bin/_lib_merge_gate.py +0 -725
  86. package/dashboard/static/assets/DoctorModal-D--qVBwe.js +0 -1
@@ -0,0 +1,708 @@
1
+ """Provider-sample staleness proofs and accounting receipts (#797 #823 #840).
2
+
3
+ Pure kernel for spec section 4.4: no I/O. The glue resolves the account's
4
+ provider samples, its accounted value, its credits and the stats index
5
+ identity from one validated snapshot and passes them in; the reducer then
6
+ receives one immutable `MemberFacts` per member.
7
+
8
+ A member's rise is refused only when its sample identity σ is *proven stale*
9
+ by a provider sample requested after both its frozen provider-activity
10
+ instant `t_m` and its receipt's completion instant `e_σ`, **and** σ holds a
11
+ valid accounting receipt. Everything else is eligible, so a window in which
12
+ no member is proven stale reduces exactly as before.
13
+
14
+ Order is always decided on precision intervals: an instant written with p
15
+ fractional digits stands for ``[k, k + 10**(9-p))`` nanoseconds, and "a is
16
+ after b" is proven only when a's interval starts at or after b's ends.
17
+ Equal instants, overlapping precision and missing instants never order.
18
+
19
+ Both axes are decided here. The weekly axis earns by (i) or (ii); the
20
+ five-hour axis earns by (i) only, against the `statusline` source's detector
21
+ baseline under the current five-hour credit generation (D11), and its
22
+ validity follows the five-hour rows of the section 4.4 table.
23
+ """
24
+ from __future__ import annotations
25
+
26
+ import dataclasses
27
+ import hashlib
28
+ import json
29
+ from typing import Callable, Collection, Iterable, Literal, Mapping, Sequence
30
+
31
+ import _lib_oauth_samples as _samples
32
+ import _lib_provider_activity as _activity
33
+
34
+
35
+ TOLERANCE_PP = 1.0
36
+ SUPPORTED_REVISIONS = ((_activity.EVIDENCE_REVISION, _activity.PARSER_REVISION),)
37
+
38
+ WEEKLY = "sevenDay"
39
+ FIVE_HOUR = "fiveHour"
40
+ AXES = (WEEKLY, FIVE_HOUR)
41
+ PATHS = ("maximal", "provider_support")
42
+ CAUSAL_KINDS = ("provider", "statusline", "manual", "legacy", "corrected", "unknown")
43
+
44
+ # Samples captured this close to the window's reset are all kept by
45
+ # `compact_samples`: a member's advertised (raw) reset lies within half an
46
+ # hour of the normalized window, and a sample captured after it proves
47
+ # nothing for that member.
48
+ _RESET_MARGIN_SECONDS = 3600
49
+ _NS = 1_000_000_000
50
+
51
+
52
+ class MalformedReceipt(ValueError):
53
+ """A receipt or credit document that does not match the codec."""
54
+
55
+
56
+ # --- instants --------------------------------------------------------------------
57
+
58
+
59
+ def _interval(text: str | None) -> tuple[int, int] | None:
60
+ """The ``[start, end)`` nanosecond interval a canonical instant denotes."""
61
+ if not isinstance(text, str):
62
+ return None
63
+ try:
64
+ start = _activity.instant_key(text)
65
+ digits = _activity.instant_precision(text)
66
+ except ValueError:
67
+ return None
68
+ return start, start + 10 ** (9 - min(digits, 9))
69
+
70
+
71
+ def after(a: str | None, b: str | None) -> bool:
72
+ """Whether instant ``a`` is proven to follow instant ``b``."""
73
+ ia, ib = _interval(a), _interval(b)
74
+ return ia is not None and ib is not None and ia[0] >= ib[1]
75
+
76
+
77
+ def _before_epoch(text: str | None, epoch: int) -> bool:
78
+ """Whether instant ``text`` is proven to precede the exact second ``epoch``."""
79
+ interval = _interval(text)
80
+ return interval is not None and interval[1] <= epoch * _NS
81
+
82
+
83
+ def _at_or_after_epoch(text: str | None, epoch: int) -> bool:
84
+ interval = _interval(text)
85
+ return interval is not None and interval[0] >= epoch * _NS
86
+
87
+
88
+ def _instant_order(text: str) -> int:
89
+ interval = _interval(text)
90
+ return -1 if interval is None else interval[0]
91
+
92
+
93
+ # --- identities --------------------------------------------------------------------
94
+
95
+
96
+ @dataclasses.dataclass(frozen=True)
97
+ class SampleIdentity:
98
+ """σ: one member's sample, keyed by everything that makes it that sample."""
99
+
100
+ token: str
101
+ activity_at_utc: str
102
+ axis: str
103
+ percent: float
104
+ window: int
105
+ evidence_revision: int
106
+ parser_revision: int
107
+
108
+ def key(self) -> str:
109
+ """A stable digest of σ, the receipt map key."""
110
+ text = json.dumps([self.token, self.activity_at_utc, self.axis,
111
+ float(self.percent), int(self.window),
112
+ self.evidence_revision, self.parser_revision],
113
+ separators=(",", ":"))
114
+ return hashlib.sha256(text.encode()).hexdigest()
115
+
116
+
117
+ @dataclasses.dataclass(frozen=True)
118
+ class Member:
119
+ """A candidate member with known evidence: σ and the raw reset it advertised."""
120
+
121
+ sigma: SampleIdentity
122
+ advertised_reset_epoch: int | None = None
123
+
124
+
125
+ @dataclasses.dataclass(frozen=True)
126
+ class MemberFacts:
127
+ refused: bool
128
+ proof_observation_id: str | None
129
+ receipt_valid: bool
130
+
131
+
132
+ ALL_ELIGIBLE = MemberFacts(False, None, False)
133
+
134
+
135
+ def sample_identity(token: str, witness, *, axis: str, percent: float,
136
+ window: int) -> SampleIdentity | None:
137
+ """σ for a member, or None when its evidence is unknown: no witness, an
138
+ unknown status, or an evidence or parser revision this kernel does not
139
+ support (an unsupported revision never proves and never earns)."""
140
+ if witness is None or witness.status != "known" or witness.instant_utc is None:
141
+ return None
142
+ if (witness.evidence_revision, witness.parser_revision) not in SUPPORTED_REVISIONS:
143
+ return None
144
+ if _interval(witness.instant_utc) is None:
145
+ return None
146
+ return SampleIdentity(token, witness.instant_utc, axis, float(percent), int(window),
147
+ witness.evidence_revision, witness.parser_revision)
148
+
149
+
150
+ def _window_text(window: int) -> str:
151
+ return _samples.normalize_weekly_reset(int(window))
152
+
153
+
154
+ def _sample_window(sample) -> str | None:
155
+ if sample.weekly_resets_at_utc is None:
156
+ return None
157
+ try:
158
+ return _samples.weekly_window(sample.weekly_resets_at_utc)
159
+ except ValueError:
160
+ return None
161
+
162
+
163
+ def _in_window(samples: Iterable, window: int) -> list:
164
+ text = _window_text(window)
165
+ return [s for s in samples
166
+ if s.weekly_percent is not None and _sample_window(s) == text]
167
+
168
+
169
+ def _five_in_window(samples: Iterable, window: int) -> list:
170
+ """The samples of one five-hour window, by canonical key equality."""
171
+ return [s for s in samples
172
+ if s.five_hour_percent is not None and s.five_hour_window_key is not None
173
+ and int(s.five_hour_window_key) == int(window)]
174
+
175
+
176
+ def _axis_window(samples: Iterable, axis: str, window: int) -> list:
177
+ return (_five_in_window(samples, window) if axis == FIVE_HOUR
178
+ else _in_window(samples, window))
179
+
180
+
181
+ def _axis_value(sample, axis: str) -> float:
182
+ return float(sample.five_hour_percent if axis == FIVE_HOUR else sample.weekly_percent)
183
+
184
+
185
+ def _latest(samples: Iterable):
186
+ best = None
187
+ for s in samples:
188
+ if best is None or (_instant_order(s.requested_at_utc), s.observation_id) > (
189
+ _instant_order(best.requested_at_utc), best.observation_id):
190
+ best = s
191
+ return best
192
+
193
+
194
+ # --- the proof -----------------------------------------------------------------------
195
+
196
+
197
+ def proven_stale(sigma: SampleIdentity, samples: Iterable, *, after_utc: str,
198
+ advertised_reset_epoch: int | None = None):
199
+ """The latest provider sample proving σ stale, or None.
200
+
201
+ A sample proves σ stale when it is in σ's authoritative window, its
202
+ request instant ``q`` follows both ``t_m`` and ``after_utc``, its raw
203
+ value is strictly more than `TOLERANCE_PP` below σ's, and its response
204
+ was captured before the member's advertised reset (the window's reset
205
+ when the member advertised none). On the five-hour axis the window is
206
+ the canonical key and the value is the sample's raw five-hour percent."""
207
+ if sigma.axis not in AXES:
208
+ return None
209
+ reset = sigma.window if advertised_reset_epoch is None else int(advertised_reset_epoch)
210
+ proving = [
211
+ s for s in _axis_window(samples, sigma.axis, sigma.window)
212
+ if _axis_value(s, sigma.axis) < sigma.percent - TOLERANCE_PP
213
+ and after(s.requested_at_utc, sigma.activity_at_utc)
214
+ and after(s.requested_at_utc, after_utc)
215
+ and _before_epoch(s.captured_at_utc, reset)
216
+ ]
217
+ return _latest(proving)
218
+
219
+
220
+ # --- accounting ------------------------------------------------------------------------
221
+
222
+
223
+ @dataclasses.dataclass(frozen=True)
224
+ class CreditTransition:
225
+ """One credit of an account and window, resolved through its causal low."""
226
+
227
+ axis: str
228
+ event_id: str
229
+ event_revision: str
230
+ causal_kind: Literal["provider", "statusline", "manual", "legacy", "corrected",
231
+ "unknown"]
232
+ requested_at_utc: str | None
233
+ # Five-hour cross-source facts (Task 5).
234
+ baseline_requested_at_utc: str | None = None
235
+ baseline_percent: float | None = None
236
+ arming_requested_at_utc: str | None = None
237
+
238
+
239
+ @dataclasses.dataclass(frozen=True)
240
+ class Accounted:
241
+ """What a completed, covered derivation left for an account, axis and window."""
242
+
243
+ account_key: str
244
+ axis: str
245
+ window: int
246
+ percent: float | None # the effective stored value of the current segment
247
+ support_id: str | None # the supporting observation or publication
248
+ support_hash: str | None # the supporting axis decision's content hash
249
+ floor_epoch: int | None # the current segment's floor
250
+ credits: tuple # the credit generation: CreditTransition, ...
251
+ coverage: str | None # the covered interpretation and frontier
252
+ detector_policy: int | None = None
253
+
254
+
255
+ @dataclasses.dataclass(frozen=True)
256
+ class Receipt:
257
+ sigma: SampleIdentity
258
+ path: Literal["maximal", "provider_support"]
259
+ account_key: str
260
+ window: int
261
+ support_id: str | None
262
+ support_hash: str | None
263
+ accounted_percent: float | None
264
+ provider_observation_id: str | None
265
+ provider_percent: float | None
266
+ provider_requested_at_utc: str | None
267
+ provider_window: str | None
268
+ detector_policy: int | None
269
+ credits: tuple # ((event_id, event_revision), ...)
270
+ coverage: str | None
271
+ completed_at_utc: str # e_σ, a completion upper bound
272
+ index_identity: str
273
+
274
+
275
+ def earn_receipts(members: Sequence[Member], accounted: Accounted, samples: Iterable, *,
276
+ completed_at_utc: str, index_identity: str) -> tuple[Receipt, ...]:
277
+ """The receipts a covered, completed derivation earns for ``members``.
278
+
279
+ (i) A member whose value equals exactly the accounted value earns a
280
+ maximal receipt; ties earn independently. (ii) Otherwise a weekly member
281
+ below the accounted value earns provider support when the admitted
282
+ provider sample that supports the completed accounting — the current
283
+ segment's latest, B — was requested after its ``t_m`` and reads raw at
284
+ or above its value. An older, higher sample the accounting has since
285
+ moved below supports nothing, and neither does the MAX-clamped stored
286
+ value. A pending or retained low, a projection fill, a numerically
287
+ dominating member and unknown evidence never earn. On the five-hour axis
288
+ the accounted value is the `statusline` source's detector baseline under
289
+ the current credit generation and only (i) exists: that baseline holds
290
+ no provider samples."""
291
+ if accounted.axis not in AXES or accounted.percent is None:
292
+ return ()
293
+ segment = [s for s in _in_window(samples, accounted.window)
294
+ if accounted.floor_epoch is None
295
+ or _at_or_after_epoch(s.captured_at_utc, accounted.floor_epoch)]
296
+ generation = tuple((c.event_id, c.event_revision) for c in accounted.credits
297
+ if c.axis == accounted.axis)
298
+ latest = _latest(segment)
299
+ earned = []
300
+ for m in members:
301
+ s = m.sigma
302
+ if s.axis != accounted.axis or s.window != accounted.window:
303
+ continue
304
+ support = None
305
+ if s.percent == accounted.percent:
306
+ path = "maximal"
307
+ elif accounted.axis == FIVE_HOUR:
308
+ continue
309
+ else:
310
+ support = latest
311
+ if (support is None or s.percent > accounted.percent
312
+ or float(support.weekly_percent) < s.percent
313
+ or not after(support.requested_at_utc, s.activity_at_utc)):
314
+ continue
315
+ path = "provider_support"
316
+ earned.append(Receipt(
317
+ sigma=s, path=path, account_key=accounted.account_key,
318
+ window=accounted.window, support_id=accounted.support_id,
319
+ support_hash=accounted.support_hash,
320
+ accounted_percent=float(accounted.percent),
321
+ provider_observation_id=None if support is None else support.observation_id,
322
+ provider_percent=None if support is None else float(support.weekly_percent),
323
+ provider_requested_at_utc=None if support is None else support.requested_at_utc,
324
+ provider_window=None if support is None else _sample_window(support),
325
+ detector_policy=accounted.detector_policy, credits=generation,
326
+ coverage=accounted.coverage, completed_at_utc=completed_at_utc,
327
+ index_identity=index_identity))
328
+ return tuple(earned)
329
+
330
+
331
+ def receipt_valid(receipt: Receipt, transitions: Iterable[CreditTransition], *,
332
+ index_identity: str, supports: Collection[str] | None = None) -> bool:
333
+ """Whether ``receipt`` stays valid against every current credit of its
334
+ account and window (the weekly rows of the section 4.4 table).
335
+
336
+ The index identity must match (a rebuild, re-upgrade or epoch change
337
+ invalidates). Every credit of the receipt's generation must still be
338
+ present at its revision (a corrected or tombstoned event invalidates).
339
+ A credit after the receipt survives only when its causal low is an
340
+ admitted provider sample requested after ``e_σ`` or a status-line reading
341
+ (R15); every other or unordered credit invalidates. When ``supports`` is
342
+ given, the receipt's supporting observation and its provider sample B
343
+ must still be among them (a held, retracted or tombstoned support
344
+ invalidates).
345
+
346
+ The five-hour rows: a `statusline`-source credit survives (its causal low
347
+ is the source's own arming observation, R15). A credit confirmed by
348
+ another source survives only when its baseline observation is an
349
+ admitted provider sample requested after ``t_m`` whose RAW five-hour
350
+ value is at least σ's, and its ARMING observation — never the later
351
+ confirming one — is a provider sample requested after ``e_σ``; every
352
+ other or unordered credit invalidates. Supports are not checked on this
353
+ axis: the accounted baseline is detector state, not a stored row."""
354
+ if receipt.sigma.axis not in AXES or receipt.index_identity != index_identity:
355
+ return False
356
+ five = receipt.sigma.axis == FIVE_HOUR
357
+ if supports is not None and not five:
358
+ if receipt.support_id is not None and receipt.support_id not in supports:
359
+ return False
360
+ if (receipt.provider_observation_id is not None
361
+ and receipt.provider_observation_id not in supports):
362
+ return False
363
+ current = {}
364
+ for t in transitions:
365
+ if t.axis == receipt.sigma.axis:
366
+ current[t.event_id] = t
367
+ generation = dict(receipt.credits)
368
+ for event_id, revision in generation.items():
369
+ t = current.get(event_id)
370
+ if t is None or t.event_revision != revision:
371
+ return False
372
+ for event_id, t in current.items():
373
+ if event_id in generation:
374
+ continue
375
+ if t.causal_kind == "statusline":
376
+ continue
377
+ if five:
378
+ if (t.causal_kind == "provider"
379
+ and t.baseline_percent is not None
380
+ and float(t.baseline_percent) >= receipt.sigma.percent
381
+ and after(t.baseline_requested_at_utc,
382
+ receipt.sigma.activity_at_utc)
383
+ and after(t.arming_requested_at_utc, receipt.completed_at_utc)):
384
+ continue
385
+ return False
386
+ if t.causal_kind == "provider" and after(t.requested_at_utc,
387
+ receipt.completed_at_utc):
388
+ continue
389
+ return False
390
+ return True
391
+
392
+
393
+ def refusal_facts(axis: str, members: Sequence[Member], samples: Iterable,
394
+ receipts: Iterable[Receipt], transitions: Iterable[CreditTransition], *,
395
+ index_identity: str, supports: Collection[str] | None = None
396
+ ) -> dict[str, MemberFacts]:
397
+ """Per-member facts for one axis, keyed by contributor token.
398
+
399
+ A member is refused only when it holds a valid receipt for its exact σ
400
+ and a sample requested after both ``t_m`` and ``e_σ`` proves it stale.
401
+ A member without a receipt is eligible with no proof reported; one whose
402
+ receipt is invalid is eligible with its proof reported. Each axis is
403
+ decided on its own members, samples and credits."""
404
+ if axis not in AXES:
405
+ return {}
406
+ samples = list(samples)
407
+ transitions = list(transitions)
408
+ by_sigma: dict[str, list[Receipt]] = {}
409
+ for r in receipts:
410
+ by_sigma.setdefault(r.sigma.key(), []).append(r)
411
+ facts: dict[str, MemberFacts] = {}
412
+ for m in members:
413
+ s = m.sigma
414
+ if s.axis != axis:
415
+ continue
416
+ held = sorted(by_sigma.get(s.key(), ()),
417
+ key=lambda r: _instant_order(r.completed_at_utc))
418
+ if not held:
419
+ facts[s.token] = ALL_ELIGIBLE
420
+ continue
421
+ valid = [r for r in held
422
+ if receipt_valid(r, transitions, index_identity=index_identity,
423
+ supports=supports)]
424
+ reset = m.advertised_reset_epoch
425
+ if valid:
426
+ strict = proven_stale(s, samples, after_utc=valid[0].completed_at_utc,
427
+ advertised_reset_epoch=reset)
428
+ else:
429
+ strict = None
430
+ proof = strict or proven_stale(s, samples, after_utc=s.activity_at_utc,
431
+ advertised_reset_epoch=reset)
432
+ facts[s.token] = MemberFacts(
433
+ refused=bool(valid) and strict is not None,
434
+ proof_observation_id=None if proof is None else proof.observation_id,
435
+ receipt_valid=bool(valid))
436
+ return facts
437
+
438
+
439
+ # --- the receipt map ---------------------------------------------------------------------
440
+
441
+
442
+ def _order(r: Receipt) -> tuple:
443
+ return (_instant_order(r.completed_at_utc), r.sigma.key())
444
+
445
+
446
+ def merge_receipts(existing: Iterable[Receipt], new: Iterable[Receipt]) -> tuple[Receipt, ...]:
447
+ """One receipt per σ, keeping the earliest completion (an earlier ``e_σ``
448
+ refuses sooner; a later one never replaces it)."""
449
+ best: dict[str, Receipt] = {}
450
+ for r in [*existing, *new]:
451
+ key = r.sigma.key()
452
+ if key not in best or _order(r) < _order(best[key]):
453
+ best[key] = r
454
+ return tuple(sorted(best.values(), key=_order))
455
+
456
+
457
+ def prune_receipts(receipts: Iterable[Receipt], transitions: Iterable[CreditTransition], *,
458
+ index_identity: str, window: int | None,
459
+ supports: Collection[str] | None = None) -> tuple[Receipt, ...]:
460
+ """The receipts still valid for the current window. An invalidated receipt
461
+ is dropped for good: it is never revived when the invalidating fact goes
462
+ away, only re-earned by new accounting with a new ``e_σ``."""
463
+ transitions = list(transitions)
464
+ return tuple(sorted(
465
+ (r for r in receipts
466
+ if window is not None and r.window == window
467
+ and receipt_valid(r, transitions, index_identity=index_identity,
468
+ supports=supports)),
469
+ key=_order))
470
+
471
+
472
+ def bounded_receipts(existing: Iterable[Receipt], new: Iterable[Receipt], *,
473
+ referenced_tokens: Collection[str],
474
+ fits: Callable[[tuple], bool]) -> tuple[Receipt, ...]:
475
+ """Merge ``new`` into ``existing`` within the document limit ``fits``.
476
+
477
+ The oldest receipt that no outstanding work references is evicted first;
478
+ when nothing unreferenced is left, new receipts are omitted (newest
479
+ first) rather than blocking accounting; as a last resort the oldest
480
+ referenced receipts go too. Eviction means eligible."""
481
+ existing = merge_receipts(existing, ())
482
+ keys = {r.sigma.key() for r in existing}
483
+ fresh = [r for r in merge_receipts((), new) if r.sigma.key() not in keys]
484
+ kept_old = list(existing)
485
+ kept_new = list(fresh)
486
+
487
+ def current() -> tuple:
488
+ return tuple(sorted([*kept_old, *kept_new], key=_order))
489
+
490
+ while not fits(current()):
491
+ evictable = [r for r in kept_old if r.sigma.token not in referenced_tokens]
492
+ if evictable:
493
+ kept_old.remove(min(evictable, key=_order))
494
+ elif kept_new:
495
+ kept_new.remove(max(kept_new, key=_order))
496
+ elif kept_old:
497
+ kept_old.remove(min(kept_old, key=_order))
498
+ else:
499
+ break
500
+ return current()
501
+
502
+
503
+ def latest_per_token(receipts: Iterable[Receipt]) -> tuple[Receipt, ...]:
504
+ """Per contributor token and axis, only the receipts of its latest frozen
505
+ instant. A contributor's provider-activity instant never moves backwards
506
+ (the scanner keeps every instant it observed), so a sample with an older
507
+ ``t_m`` is never presented again; its receipt can authorize nothing."""
508
+ receipts = list(receipts)
509
+ latest: dict[tuple[str, str], int] = {}
510
+ for r in receipts:
511
+ key = (r.sigma.token, r.sigma.axis)
512
+ latest[key] = max(latest.get(key, -1), _instant_order(r.sigma.activity_at_utc))
513
+ return tuple(sorted(
514
+ (r for r in receipts
515
+ if _instant_order(r.sigma.activity_at_utc) == latest[(r.sigma.token, r.sigma.axis)]),
516
+ key=_order))
517
+
518
+
519
+ def floor_microsecond(text: str) -> str | None:
520
+ """A canonical instant truncated to whole microseconds and stepped back
521
+ one, in the provider samples' ``q`` form: every ``q`` proven after
522
+ ``text`` compares strictly greater than it as text."""
523
+ interval = _interval(text)
524
+ if interval is None:
525
+ return None
526
+ import datetime as _dt
527
+ micros = interval[0] // 1000 - 1
528
+ instant = _dt.datetime(1970, 1, 1, tzinfo=_dt.timezone.utc) + _dt.timedelta(
529
+ microseconds=micros)
530
+ return _samples.format_request_started(instant)
531
+
532
+
533
+ # --- the compact sample set ------------------------------------------------------------------
534
+
535
+
536
+ def compact_samples(samples: Iterable, *, window: int,
537
+ floor_epoch: int | None, axis: str = WEEKLY) -> tuple:
538
+ """A subset of the window's samples that yields the same `proven_stale`
539
+ and `earn_receipts` answers for every member (both pick the latest
540
+ qualifying sample, and qualification is monotone in ``q`` within one
541
+ value): per weekly value, the latest sample and the latest captured at
542
+ or after the floor, plus every sample captured within an hour of the
543
+ window's reset (a member's advertised reset may precede the window).
544
+ ``axis`` selects the window identity and value (five-hour: the canonical
545
+ key and the raw five-hour percent)."""
546
+ margin = int(window) - _RESET_MARGIN_SECONDS
547
+ keep: dict[str, object] = {}
548
+ latest: dict[float, object] = {}
549
+ latest_floor: dict[float, object] = {}
550
+ for s in _axis_window(samples, axis, window):
551
+ if not _before_epoch(s.captured_at_utc, margin):
552
+ keep[s.observation_id] = s
553
+ continue
554
+ v = _axis_value(s, axis)
555
+ latest[v] = _latest([x for x in (latest.get(v), s) if x is not None])
556
+ if floor_epoch is None or _at_or_after_epoch(s.captured_at_utc, floor_epoch):
557
+ latest_floor[v] = _latest([x for x in (latest_floor.get(v), s) if x is not None])
558
+ for s in [*latest.values(), *latest_floor.values()]:
559
+ keep[s.observation_id] = s
560
+ return tuple(sorted(keep.values(),
561
+ key=lambda s: (_instant_order(s.requested_at_utc), s.observation_id)))
562
+
563
+
564
+ # --- codecs ------------------------------------------------------------------------------------
565
+
566
+
567
+ _SIGMA_KEYS = ("token", "activityAt", "axis", "percent", "window", "evidenceRevision",
568
+ "parserRevision")
569
+ _RECEIPT_KEYS = frozenset({
570
+ "sigma", "path", "account", "window", "supportId", "supportHash", "accountedPercent",
571
+ "providerObservationId", "providerPercent", "providerRequestedAt", "providerWindow",
572
+ "detectorPolicy", "credits", "coverage", "completedAt", "indexIdentity"})
573
+ _CREDIT_KEYS = frozenset({
574
+ "axis", "eventId", "eventRevision", "causalKind", "requestedAt", "baselineRequestedAt",
575
+ "baselinePercent", "armingRequestedAt"})
576
+
577
+
578
+ def _number(value) -> float:
579
+ if isinstance(value, bool) or not isinstance(value, (int, float)):
580
+ raise MalformedReceipt("not a number")
581
+ return float(value)
582
+
583
+
584
+ def _opt_number(value) -> float | None:
585
+ return None if value is None else _number(value)
586
+
587
+
588
+ def _int(value) -> int:
589
+ if isinstance(value, bool) or not isinstance(value, int):
590
+ raise MalformedReceipt("not an integer")
591
+ return value
592
+
593
+
594
+ def _opt_int(value) -> int | None:
595
+ return None if value is None else _int(value)
596
+
597
+
598
+ def _text(value) -> str:
599
+ if not isinstance(value, str) or not value:
600
+ raise MalformedReceipt("not a string")
601
+ return value
602
+
603
+
604
+ def _opt_text(value) -> str | None:
605
+ return None if value is None else _text(value)
606
+
607
+
608
+ def _instant(value) -> str:
609
+ if _interval(_text(value)) is None:
610
+ raise MalformedReceipt("not a canonical instant")
611
+ return value
612
+
613
+
614
+ def _opt_instant(value) -> str | None:
615
+ return None if value is None else _instant(value)
616
+
617
+
618
+ def _exact(doc, keys) -> dict:
619
+ if not isinstance(doc, dict) or set(doc) != set(keys):
620
+ raise MalformedReceipt("unexpected keys")
621
+ return doc
622
+
623
+
624
+ def _sigma_to_json(s: SampleIdentity) -> dict:
625
+ return {"token": s.token, "activityAt": s.activity_at_utc, "axis": s.axis,
626
+ "percent": s.percent, "window": s.window,
627
+ "evidenceRevision": s.evidence_revision, "parserRevision": s.parser_revision}
628
+
629
+
630
+ def _sigma_from_json(doc) -> SampleIdentity:
631
+ doc = _exact(doc, _SIGMA_KEYS)
632
+ axis = _text(doc["axis"])
633
+ if axis not in AXES:
634
+ raise MalformedReceipt("unknown axis")
635
+ return SampleIdentity(_text(doc["token"]), _instant(doc["activityAt"]), axis,
636
+ _number(doc["percent"]), _int(doc["window"]),
637
+ _int(doc["evidenceRevision"]), _int(doc["parserRevision"]))
638
+
639
+
640
+ def receipt_to_json(r: Receipt) -> dict:
641
+ return {
642
+ "sigma": _sigma_to_json(r.sigma), "path": r.path, "account": r.account_key,
643
+ "window": r.window, "supportId": r.support_id, "supportHash": r.support_hash,
644
+ "accountedPercent": r.accounted_percent,
645
+ "providerObservationId": r.provider_observation_id,
646
+ "providerPercent": r.provider_percent,
647
+ "providerRequestedAt": r.provider_requested_at_utc,
648
+ "providerWindow": r.provider_window, "detectorPolicy": r.detector_policy,
649
+ "credits": [[event_id, revision] for event_id, revision in r.credits],
650
+ "coverage": r.coverage, "completedAt": r.completed_at_utc,
651
+ "indexIdentity": r.index_identity,
652
+ }
653
+
654
+
655
+ def receipt_from_json(doc) -> Receipt:
656
+ doc = _exact(doc, _RECEIPT_KEYS)
657
+ path = doc["path"]
658
+ if path not in PATHS:
659
+ raise MalformedReceipt("unknown path")
660
+ credits = doc["credits"]
661
+ if not isinstance(credits, list):
662
+ raise MalformedReceipt("credits")
663
+ pairs = []
664
+ for pair in credits:
665
+ if not isinstance(pair, list) or len(pair) != 2:
666
+ raise MalformedReceipt("credit pair")
667
+ pairs.append((_text(pair[0]), _text(pair[1])))
668
+ r = Receipt(
669
+ sigma=_sigma_from_json(doc["sigma"]), path=path, account_key=_text(doc["account"]),
670
+ window=_int(doc["window"]), support_id=_opt_text(doc["supportId"]),
671
+ support_hash=_opt_text(doc["supportHash"]),
672
+ accounted_percent=_opt_number(doc["accountedPercent"]),
673
+ provider_observation_id=_opt_text(doc["providerObservationId"]),
674
+ provider_percent=_opt_number(doc["providerPercent"]),
675
+ provider_requested_at_utc=_opt_instant(doc["providerRequestedAt"]),
676
+ provider_window=_opt_text(doc["providerWindow"]),
677
+ detector_policy=_opt_int(doc["detectorPolicy"]), credits=tuple(pairs),
678
+ coverage=_opt_text(doc["coverage"]), completed_at_utc=_instant(doc["completedAt"]),
679
+ index_identity=_text(doc["indexIdentity"]))
680
+ if (path == "provider_support") != (r.provider_observation_id is not None):
681
+ raise MalformedReceipt("path and provider support disagree")
682
+ return r
683
+
684
+
685
+ def credit_to_json(c: CreditTransition) -> dict:
686
+ return {"axis": c.axis, "eventId": c.event_id, "eventRevision": c.event_revision,
687
+ "causalKind": c.causal_kind, "requestedAt": c.requested_at_utc,
688
+ "baselineRequestedAt": c.baseline_requested_at_utc,
689
+ "baselinePercent": c.baseline_percent,
690
+ "armingRequestedAt": c.arming_requested_at_utc}
691
+
692
+
693
+ def credit_from_json(doc) -> CreditTransition:
694
+ doc = _exact(doc, _CREDIT_KEYS)
695
+ axis = _text(doc["axis"])
696
+ kind = doc["causalKind"]
697
+ if axis not in AXES or kind not in CAUSAL_KINDS:
698
+ raise MalformedReceipt("credit axis or kind")
699
+ return CreditTransition(
700
+ axis=axis, event_id=_text(doc["eventId"]), event_revision=_text(doc["eventRevision"]),
701
+ causal_kind=kind, requested_at_utc=_opt_instant(doc["requestedAt"]),
702
+ baseline_requested_at_utc=_opt_instant(doc["baselineRequestedAt"]),
703
+ baseline_percent=_opt_number(doc["baselinePercent"]),
704
+ arming_requested_at_utc=_opt_instant(doc["armingRequestedAt"]))
705
+
706
+
707
+ def receipts_by_key(receipts: Iterable[Receipt]) -> Mapping[str, Receipt]:
708
+ return {r.sigma.key(): r for r in receipts}