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.
- package/CHANGELOG.md +41 -0
- package/bin/_cctally_account.py +32 -8
- package/bin/_cctally_cache.py +4491 -295
- package/bin/_cctally_completion.py +1355 -0
- package/bin/_cctally_config.py +37 -0
- package/bin/_cctally_core.py +282 -9
- package/bin/_cctally_dashboard.py +8 -11
- package/bin/_cctally_dashboard_conversation.py +292 -23
- package/bin/_cctally_dashboard_envelope.py +42 -35
- package/bin/_cctally_dashboard_sources.py +18 -9
- package/bin/_cctally_db.py +401 -9
- package/bin/_cctally_doctor.py +469 -27
- package/bin/_cctally_five_hour.py +166 -1049
- package/bin/_cctally_forecast.py +81 -9
- package/bin/_cctally_journal.py +5076 -177
- package/bin/_cctally_journal_repair.py +110 -19
- package/bin/_cctally_oauth_samples.py +280 -0
- package/bin/_cctally_parser.py +47 -0
- package/bin/_cctally_project.py +8 -2
- package/bin/_cctally_quarantine.py +662 -0
- package/bin/_cctally_quota.py +96 -12
- package/bin/_cctally_quota_model.py +391 -83
- package/bin/_cctally_record.py +1088 -269
- package/bin/_cctally_rederive.py +854 -192
- package/bin/_cctally_refresh.py +113 -13
- package/bin/_cctally_source_analytics.py +23 -2
- package/bin/_cctally_statusline.py +7062 -505
- package/bin/_cctally_store.py +1244 -91
- package/bin/_cctally_transcript.py +8 -7
- package/bin/_cctally_tui.py +119 -60
- package/bin/_cctally_update.py +25 -0
- package/bin/_lib_account_binding.py +152 -0
- package/bin/_lib_account_protocol.py +569 -0
- package/bin/_lib_aggregators.py +4 -0
- package/bin/_lib_cache_coverage.py +13 -1
- package/bin/_lib_cache_writer_lock.py +19 -3
- package/bin/_lib_clone_copy.py +177 -0
- package/bin/_lib_codex_conversation_query.py +133 -19
- package/bin/_lib_codex_conversation_watch.py +59 -14
- package/bin/_lib_codex_drain_admission.py +214 -0
- package/bin/_lib_codex_metadata.py +18 -1
- package/bin/_lib_codex_publication_barrier.py +277 -0
- package/bin/_lib_codex_rollout_container.py +502 -0
- package/bin/_lib_conversation_dispatch.py +3 -0
- package/bin/_lib_conversation_query.py +11 -3
- package/bin/_lib_dashboard_sources.py +57 -15
- package/bin/_lib_detector_access_registry.py +4038 -0
- package/bin/_lib_doctor.py +753 -1
- package/bin/_lib_five_hour.py +126 -0
- package/bin/_lib_ingest_frontier.py +414 -20
- package/bin/_lib_journal.py +261 -10
- package/bin/_lib_journal_router.py +166 -48
- package/bin/_lib_jsonl.py +5 -4
- package/bin/_lib_legacy_spool.py +126 -0
- package/bin/_lib_legacy_writer_evidence.py +136 -0
- package/bin/_lib_meter_rate_change.py +10 -0
- package/bin/_lib_oauth_samples.py +238 -0
- package/bin/_lib_outcome_quarantine.py +690 -0
- package/bin/_lib_pricing.py +7 -2
- package/bin/_lib_pricing_check.py +0 -12
- package/bin/_lib_provider_activity.py +770 -0
- package/bin/_lib_quarantine.py +155 -0
- package/bin/_lib_quota_calibration.py +53 -8
- package/bin/_lib_quota_copy.py +2 -1
- package/bin/_lib_quota_model.py +384 -52
- package/bin/_lib_rebuild_progress.py +146 -0
- package/bin/_lib_record.py +22 -0
- package/bin/_lib_rederive.py +393 -8
- package/bin/_lib_segment_summary.py +35 -2
- package/bin/_lib_snapshot_cache.py +43 -1
- package/bin/_lib_stale_proof.py +708 -0
- package/bin/_lib_statusline_candidates.py +356 -27
- package/bin/_lib_worker_launches.py +315 -0
- package/bin/cctally +29 -12
- package/dashboard/static/assets/{ConversationsView-Bc8LCOAZ.js → ConversationsView-Dflb_myA.js} +1 -1
- package/dashboard/static/assets/DoctorModal-DFX5CfIs.js +1 -0
- package/dashboard/static/assets/{ModalRoot-DLPgyAM_.js → ModalRoot-HlxoagIa.js} +1 -1
- package/dashboard/static/assets/{ProjectsDrillPanel-z53a4v4M.js → ProjectsDrillPanel-C3PJy1xx.js} +1 -1
- package/dashboard/static/assets/{SourceDetailModal-Bw0T8W-W.js → SourceDetailModal-BczsWKhD.js} +1 -1
- package/dashboard/static/assets/{UpdateModal-D5AKx4Yw.js → UpdateModal-Os-Lo5cs.js} +1 -1
- package/dashboard/static/assets/{index-BXDOzSng.js → index-CYZ3NLqF.js} +3 -3
- package/dashboard/static/assets/{outlineNavigation-JObY7cUY.js → outlineNavigation-k-zIif6W.js} +1 -1
- package/dashboard/static/dashboard.html +1 -1
- package/package.json +20 -2
- package/bin/_lib_merge_gate.py +0 -725
- 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}
|