cctally 1.114.0 → 1.115.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/bin/_cctally_account.py +32 -8
- package/bin/_cctally_cache.py +4394 -279
- package/bin/_cctally_completion.py +1287 -0
- package/bin/_cctally_config.py +37 -0
- package/bin/_cctally_core.py +178 -2
- package/bin/_cctally_dashboard.py +2 -2
- package/bin/_cctally_dashboard_conversation.py +292 -23
- package/bin/_cctally_dashboard_envelope.py +16 -3
- package/bin/_cctally_dashboard_sources.py +18 -9
- package/bin/_cctally_db.py +316 -5
- package/bin/_cctally_doctor.py +436 -27
- package/bin/_cctally_five_hour.py +82 -31
- package/bin/_cctally_forecast.py +81 -9
- package/bin/_cctally_journal.py +3884 -112
- package/bin/_cctally_journal_repair.py +101 -12
- package/bin/_cctally_parser.py +42 -0
- package/bin/_cctally_project.py +8 -2
- package/bin/_cctally_quarantine.py +652 -0
- package/bin/_cctally_quota.py +26 -4
- package/bin/_cctally_quota_model.py +360 -83
- package/bin/_cctally_record.py +677 -149
- package/bin/_cctally_rederive.py +297 -32
- package/bin/_cctally_refresh.py +84 -13
- package/bin/_cctally_source_analytics.py +23 -2
- package/bin/_cctally_statusline.py +6137 -517
- package/bin/_cctally_store.py +287 -48
- package/bin/_cctally_transcript.py +8 -7
- package/bin/_cctally_tui.py +57 -53
- package/bin/_lib_account_binding.py +152 -0
- package/bin/_lib_account_protocol.py +386 -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 +142 -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_detector_access_registry.py +3817 -0
- package/bin/_lib_doctor.py +452 -1
- package/bin/_lib_five_hour.py +126 -0
- package/bin/_lib_ingest_frontier.py +414 -20
- package/bin/_lib_journal.py +141 -10
- 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_merge_gate.py +29 -302
- package/bin/_lib_meter_rate_change.py +10 -0
- package/bin/_lib_outcome_quarantine.py +690 -0
- package/bin/_lib_pricing.py +7 -2
- package/bin/_lib_quarantine.py +154 -0
- package/bin/_lib_quota_calibration.py +53 -8
- package/bin/_lib_quota_copy.py +2 -1
- package/bin/_lib_quota_model.py +288 -46
- package/bin/_lib_rederive.py +94 -5
- package/bin/_lib_segment_summary.py +35 -2
- package/bin/_lib_statusline_candidates.py +292 -23
- package/bin/cctally +23 -1
- package/package.json +14 -1
|
@@ -18,12 +18,14 @@ import contextlib
|
|
|
18
18
|
import dataclasses
|
|
19
19
|
import datetime as dt
|
|
20
20
|
import fcntl
|
|
21
|
+
import hashlib
|
|
21
22
|
import json
|
|
22
23
|
import os
|
|
23
24
|
import sqlite3
|
|
24
25
|
import sys
|
|
25
26
|
|
|
26
27
|
import _cctally_core
|
|
28
|
+
import _cctally_store
|
|
27
29
|
import _lib_quota_model as qm
|
|
28
30
|
from _lib_quota_model import ( # re-exported for callers and tests
|
|
29
31
|
BlockingReason, CalibrationEvidence, CalibrationStatus, Verdict,
|
|
@@ -56,36 +58,59 @@ def eprint(*args, **kwargs):
|
|
|
56
58
|
# Part III section 28 require: the kernel has no producer for that status and
|
|
57
59
|
# receives it through `analyse(extra_statuses=...)`.
|
|
58
60
|
# ---------------------------------------------------------------------------
|
|
59
|
-
#: First UTC date
|
|
60
|
-
#:
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
#:
|
|
65
|
-
#:
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
61
|
+
#: First UTC date of the earliest validated coefficient era. Every
|
|
62
|
+
#: observation before it is covered by no era and is never fitted; the
|
|
63
|
+
#: analysis itself starts at the start of the era the clock selects (#978).
|
|
64
|
+
SUPPORTED_COMPOSITION_FROM: dt.date = qm.COEFFICIENT_ERAS[0].start
|
|
65
|
+
|
|
66
|
+
#: The history before every validated era, published first in
|
|
67
|
+
#: `method.coefficientEras` so a later reader can re-test the boundary rather
|
|
68
|
+
#: than inherit it. It carries `id: None` and no weights, because no
|
|
69
|
+
#: coefficient set is validated for it.
|
|
70
|
+
UNVALIDATED_ERA_RECORD: dict = {
|
|
71
|
+
"id": None,
|
|
72
|
+
"from": None,
|
|
73
|
+
"until": SUPPORTED_COMPOSITION_FROM.isoformat(),
|
|
74
|
+
"state": "unvalidated",
|
|
75
|
+
"provenance": (
|
|
76
|
+
"docs/quota-model.md: the Opus 4.8 era fits at about 1,989,101 "
|
|
77
|
+
"units per point against 2.4M for Opus 5, a budget difference the "
|
|
78
|
+
"shared token-class coefficients do not absorb. The shipped "
|
|
79
|
+
"estimator gives 1,265,184 to 1,479,027 over the same weeks, and "
|
|
80
|
+
"spec section 6 declines to adjudicate between the two."
|
|
81
|
+
),
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def published_coefficient_eras() -> list:
|
|
86
|
+
"""The complete era catalogue in order, as `method.coefficientEras`.
|
|
87
|
+
|
|
88
|
+
The unvalidated record first, then each kernel era with its interval,
|
|
89
|
+
provenance and token-class weights. Built from `qm.COEFFICIENT_ERAS` on
|
|
90
|
+
every call, so the published table can never drift from the one the
|
|
91
|
+
kernel weights with.
|
|
92
|
+
"""
|
|
93
|
+
out = [dict(UNVALIDATED_ERA_RECORD)]
|
|
94
|
+
for era in qm.COEFFICIENT_ERAS:
|
|
95
|
+
out.append({
|
|
96
|
+
"id": era.id,
|
|
97
|
+
"from": era.start.isoformat(),
|
|
98
|
+
"until": None if era.end is None else era.end.isoformat(),
|
|
99
|
+
"state": "validated",
|
|
100
|
+
"provenance": era.provenance,
|
|
101
|
+
"tokenClassWeights": era.weight_map(),
|
|
102
|
+
})
|
|
103
|
+
return out
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def select_era(now: "dt.datetime") -> "qm.CoefficientEra | None":
|
|
107
|
+
"""The one era an analysis at `now` is fitted in (#978 spec 4.3).
|
|
108
|
+
|
|
109
|
+
The clock decides, never the data: the real clock, or `CCTALLY_AS_OF` in
|
|
110
|
+
harnesses. None before 2026-07-25, where no era is validated.
|
|
111
|
+
"""
|
|
112
|
+
return qm.era_for_instant(now)
|
|
113
|
+
|
|
89
114
|
|
|
90
115
|
#: Snapshot sources that are synthetic by construction and are never read as
|
|
91
116
|
#: observations (spec section 4). A closed committed set: `record-credit`
|
|
@@ -229,6 +254,23 @@ def _probe_bundle(stats_conn, cache_conn) -> tuple:
|
|
|
229
254
|
)
|
|
230
255
|
|
|
231
256
|
|
|
257
|
+
#: How far below the analysis start the SQL text prefilter reaches. Stored
|
|
258
|
+
#: timestamps may carry a non-UTC offset, and text comparison then orders
|
|
259
|
+
#: them by local wall time: `2026-09-22T03:00:00+05:00` sorts at or after
|
|
260
|
+
#: `2026-09-22T00:00:00+00:00` while naming an instant before it, and
|
|
261
|
+
#: `2026-09-21T22:30:00-05:00` sorts before it while naming an instant after
|
|
262
|
+
#: it. Any real offset is under a day, so a one-day margin keeps every
|
|
263
|
+
#: in-range row, and the exact bound is then applied to the parsed instant.
|
|
264
|
+
#: The kernel refuses an out-of-era instant outright (#978 spec 4.2,
|
|
265
|
+
#: invariant 5), so the bound has to be exact rather than textual.
|
|
266
|
+
_TEXT_PREFILTER_MARGIN = dt.timedelta(days=1)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _text_prefilter_floor(start) -> str:
|
|
270
|
+
"""The coarse SQL lower bound for an exact instant bound `start`."""
|
|
271
|
+
return (start - _TEXT_PREFILTER_MARGIN).isoformat()
|
|
272
|
+
|
|
273
|
+
|
|
232
274
|
def _read_stats_component(conn, account_key, start):
|
|
233
275
|
"""Meter readings and authoritative credit instants for one account.
|
|
234
276
|
|
|
@@ -250,7 +292,7 @@ def _read_stats_component(conn, account_key, start):
|
|
|
250
292
|
params: list = list(SYNTHETIC_SNAPSHOT_SOURCES)
|
|
251
293
|
if start is not None:
|
|
252
294
|
sql += " AND captured_at_utc >= ?"
|
|
253
|
-
params.append(start
|
|
295
|
+
params.append(_text_prefilter_floor(start))
|
|
254
296
|
clause, extra = _account_clause("account_key", account_key)
|
|
255
297
|
sql += clause
|
|
256
298
|
params.extend(extra)
|
|
@@ -263,6 +305,8 @@ def _read_stats_component(conn, account_key, start):
|
|
|
263
305
|
at = parse_instant(captured, "captured_at_utc")
|
|
264
306
|
if at is None:
|
|
265
307
|
continue
|
|
308
|
+
if start is not None and at < start:
|
|
309
|
+
continue
|
|
266
310
|
anchor = parse_instant(week_at, "week_start_at")
|
|
267
311
|
if anchor is None:
|
|
268
312
|
# A pre-`week_start_at` row. Its date-only boundary is read as
|
|
@@ -301,9 +345,13 @@ def _read_stats_component(conn, account_key, start):
|
|
|
301
345
|
return snapshots, credits, diagnostics
|
|
302
346
|
|
|
303
347
|
|
|
304
|
-
def _read_cache_component(conn, account_key, start):
|
|
348
|
+
def _read_cache_component(conn, account_key, start, end=None):
|
|
305
349
|
"""Priced requests for one account, plus the store-wide ingest tail.
|
|
306
350
|
|
|
351
|
+
Rows are bounded to `[start, end)` on the parsed instant before the
|
|
352
|
+
fast-mode count, so `fastModeEntriesExcluded` describes the analysed
|
|
353
|
+
window; `end` is a closed era's exclusive end, or None for an open one.
|
|
354
|
+
|
|
307
355
|
`newest_entry_at` is deliberately NOT account-scoped. It answers "has the
|
|
308
356
|
ingest finished covering this day", which is a property of the store; an
|
|
309
357
|
account-scoped maximum would report every day of a dormant account as
|
|
@@ -318,7 +366,7 @@ def _read_cache_component(conn, account_key, start):
|
|
|
318
366
|
params: list = []
|
|
319
367
|
if start is not None:
|
|
320
368
|
sql += " AND timestamp_utc >= ?"
|
|
321
|
-
params.append(start
|
|
369
|
+
params.append(_text_prefilter_floor(start))
|
|
322
370
|
clause, extra = _account_clause("account_key", account_key)
|
|
323
371
|
sql += clause
|
|
324
372
|
params.extend(extra)
|
|
@@ -330,6 +378,10 @@ def _read_cache_component(conn, account_key, start):
|
|
|
330
378
|
at = parse_instant(row[0], "timestamp_utc")
|
|
331
379
|
if at is None:
|
|
332
380
|
continue
|
|
381
|
+
if start is not None and at < start:
|
|
382
|
+
continue
|
|
383
|
+
if end is not None and at >= end:
|
|
384
|
+
continue
|
|
333
385
|
if row[7] == "fast":
|
|
334
386
|
fast_excluded += 1
|
|
335
387
|
continue
|
|
@@ -369,26 +421,74 @@ def _retained_snapshot_span(conn, account_key):
|
|
|
369
421
|
return parse_instant(row[0]), parse_instant(row[1])
|
|
370
422
|
|
|
371
423
|
|
|
372
|
-
def era_floor_instant() -> "dt.datetime":
|
|
373
|
-
"""The first instant
|
|
374
|
-
return
|
|
375
|
-
SUPPORTED_COMPOSITION_FROM, dt.time(0, 0), tzinfo=UTC)
|
|
424
|
+
def era_floor_instant(era) -> "dt.datetime":
|
|
425
|
+
"""The first instant `era`'s coefficients are validated for."""
|
|
426
|
+
return qm.era_bounds(era)[0]
|
|
376
427
|
|
|
377
428
|
|
|
378
|
-
def resolve_analysis_start(since):
|
|
379
|
-
"""`max(--since, the era
|
|
429
|
+
def resolve_analysis_start(since, era):
|
|
430
|
+
"""`max(--since, the selected era's start)`.
|
|
380
431
|
|
|
381
|
-
A `--since` earlier than the
|
|
432
|
+
A `--since` earlier than the era does not widen the window: section 6
|
|
382
433
|
says an era whose composition the coefficients are not validated for is
|
|
383
|
-
not fitted, and
|
|
434
|
+
not fitted, and #978 spec 4.3 says neither `--since` nor `--watch-from`
|
|
435
|
+
reopens a preceding era, so a flag cannot licence fitting one.
|
|
384
436
|
"""
|
|
385
|
-
floor = era_floor_instant()
|
|
437
|
+
floor = era_floor_instant(era)
|
|
386
438
|
if since is None:
|
|
387
439
|
return floor
|
|
388
440
|
return max(since, floor)
|
|
389
441
|
|
|
390
442
|
|
|
391
|
-
def
|
|
443
|
+
def week_spans_era(week: "CurrentWeek", era) -> bool:
|
|
444
|
+
"""Is the subscription week not wholly inside `era`?
|
|
445
|
+
|
|
446
|
+
Either the week starts before the era, or, for a closed era a historical
|
|
447
|
+
clock selected, it ends after the era's exclusive end. Such a week's
|
|
448
|
+
units would come from two eras, so its modelled figures are withheld
|
|
449
|
+
(#978 spec 4.3).
|
|
450
|
+
"""
|
|
451
|
+
start, end = qm.era_bounds(era)
|
|
452
|
+
return week.anchor < start or (end is not None and week.end > end)
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def current_week_units(population, era) -> float:
|
|
456
|
+
"""Summed weighted units of the current week's general-meter entries.
|
|
457
|
+
|
|
458
|
+
A dedicated-pool family contributes nothing, and an entry whose
|
|
459
|
+
cache-write split is unknown is skipped, as `population_units` does. An
|
|
460
|
+
entry `era` does not cover raises `CoefficientEraMismatch` instead of
|
|
461
|
+
being skipped (#978 spec 4.2, invariant 5): the week's units are never
|
|
462
|
+
silently clipped to the part one era covers.
|
|
463
|
+
"""
|
|
464
|
+
total = 0.0
|
|
465
|
+
for entry in population:
|
|
466
|
+
if qm.family_participation(
|
|
467
|
+
qm.normalize_family(entry.model)) != "general":
|
|
468
|
+
continue
|
|
469
|
+
weighed = qm.weigh_entry(entry, era)
|
|
470
|
+
if weighed.cause == qm.ERA_UNAVAILABLE:
|
|
471
|
+
raise qm.CoefficientEraMismatch(
|
|
472
|
+
f"entry.at {entry.at.isoformat()} lies outside coefficient "
|
|
473
|
+
f"era {era.id!r}")
|
|
474
|
+
if weighed.units is None:
|
|
475
|
+
continue
|
|
476
|
+
total += weighed.units
|
|
477
|
+
return total
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def _confine(rows, end):
|
|
481
|
+
"""The rows strictly before a closed era's exclusive `end`.
|
|
482
|
+
|
|
483
|
+
The lower bound is already applied at the query; an open era (`end`
|
|
484
|
+
None) is bounded by the clock exactly as before #978.
|
|
485
|
+
"""
|
|
486
|
+
if end is None:
|
|
487
|
+
return list(rows)
|
|
488
|
+
return [row for row in rows if row.at < end]
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def load_population(account_key, *, since, now, era) -> LoadResult:
|
|
392
492
|
"""Read both stores coherently and return one account's population.
|
|
393
493
|
|
|
394
494
|
The stores are opened through the guarded helpers, so `CCTALLY_DATA_DIR`,
|
|
@@ -396,9 +496,21 @@ def load_population(account_key, *, since, now) -> LoadResult:
|
|
|
396
496
|
all apply. Every failure is caught and reported as a typed `unavailable`
|
|
397
497
|
result; an uncaught `OperationalError` from a store read is never allowed
|
|
398
498
|
to reach the user.
|
|
499
|
+
|
|
500
|
+
`era` is the era `select_era(now)` chose. Snapshots, credits and entries
|
|
501
|
+
are confined to `[analysis start, era end)` before anything downstream
|
|
502
|
+
segments, fences or fits them (#978 spec 4.3). A None era — a clock
|
|
503
|
+
before every validated era — reads nothing and returns the
|
|
504
|
+
`unvalidated-coefficient-era` status, because no coefficient set could
|
|
505
|
+
weigh what it would read.
|
|
399
506
|
"""
|
|
400
507
|
qm.require_aware(now, "now")
|
|
401
|
-
|
|
508
|
+
if era is None:
|
|
509
|
+
return LoadResult(
|
|
510
|
+
status=CalibrationStatus.UNVALIDATED_COEFFICIENT_ERA,
|
|
511
|
+
cause="history-predates-supported-composition")
|
|
512
|
+
start = resolve_analysis_start(since, era)
|
|
513
|
+
era_end = qm.era_bounds(era)[1]
|
|
402
514
|
stats_conn = None
|
|
403
515
|
cache_conn = None
|
|
404
516
|
try:
|
|
@@ -419,7 +531,7 @@ def load_population(account_key, *, since, now) -> LoadResult:
|
|
|
419
531
|
snapshots, credits, diagnostics = _read_stats_component(
|
|
420
532
|
stats_conn, account_key, start)
|
|
421
533
|
entries, newest, fast_excluded = _read_cache_component(
|
|
422
|
-
cache_conn, account_key, start)
|
|
534
|
+
cache_conn, account_key, start, era_end)
|
|
423
535
|
except sqlite3.Error:
|
|
424
536
|
return LoadResult(analysis_start=start,
|
|
425
537
|
status=CalibrationStatus.UNAVAILABLE,
|
|
@@ -430,6 +542,18 @@ def load_population(account_key, *, since, now) -> LoadResult:
|
|
|
430
542
|
cause="malformed-retained-instant")
|
|
431
543
|
after = _probe_bundle(stats_conn, cache_conn)
|
|
432
544
|
if before == after:
|
|
545
|
+
# #825/#832 spec 6: the stats half of a population this
|
|
546
|
+
# external plan carries into ingest must come from the current
|
|
547
|
+
# interpretation. The coherence bracket proves the reads saw
|
|
548
|
+
# one unchanged store (a republication moves the probe), so the
|
|
549
|
+
# identity of that store is checked at the bracket's edge.
|
|
550
|
+
try:
|
|
551
|
+
with _cctally_store.current_stats_snapshot(stats_conn):
|
|
552
|
+
pass
|
|
553
|
+
except _cctally_store.StatsIndexNotCurrent:
|
|
554
|
+
return LoadResult(analysis_start=start,
|
|
555
|
+
status=CalibrationStatus.UNAVAILABLE,
|
|
556
|
+
cause="index-not-current")
|
|
433
557
|
payload = (earliest, snapshots, credits, diagnostics,
|
|
434
558
|
entries, newest, fast_excluded)
|
|
435
559
|
break
|
|
@@ -451,6 +575,9 @@ def load_population(account_key, *, since, now) -> LoadResult:
|
|
|
451
575
|
|
|
452
576
|
earliest, snapshots, credits, diagnostics, entries, newest, \
|
|
453
577
|
fast_excluded = payload
|
|
578
|
+
snapshots = _confine(snapshots, era_end)
|
|
579
|
+
credits = _confine(credits, era_end)
|
|
580
|
+
entries = _confine(entries, era_end)
|
|
454
581
|
diagnostics = dict(diagnostics)
|
|
455
582
|
diagnostics["analysisStart"] = start.isoformat()
|
|
456
583
|
diagnostics["fastModeEntriesExcluded"] = fast_excluded
|
|
@@ -993,6 +1120,10 @@ class PersistMode:
|
|
|
993
1120
|
now: "dt.datetime"
|
|
994
1121
|
fingerprint: str
|
|
995
1122
|
regime_start: "dt.datetime"
|
|
1123
|
+
#: The id of the coefficient era the clock selected (#978), written on
|
|
1124
|
+
#: every regime. None when the clock precedes every validated era, which
|
|
1125
|
+
#: makes the run persist nothing.
|
|
1126
|
+
coefficient_era: "str | None"
|
|
996
1127
|
|
|
997
1128
|
|
|
998
1129
|
def _interval_json(interval):
|
|
@@ -1008,6 +1139,7 @@ def _regime_from(evidence, *, effective_from, mode, status):
|
|
|
1008
1139
|
"effectiveUntil": None,
|
|
1009
1140
|
"fingerprint": mode.fingerprint,
|
|
1010
1141
|
"algorithmRevision": qm.QUOTA_MODEL_ALGORITHM_REVISION,
|
|
1142
|
+
"coefficientEra": mode.coefficient_era,
|
|
1011
1143
|
"unitsPerPoint": evidence.value,
|
|
1012
1144
|
"interval": _interval_json(evidence.interval),
|
|
1013
1145
|
"support": ({"days": evidence.support.days,
|
|
@@ -1070,6 +1202,87 @@ def stored_regimes(state: dict, account_key) -> list:
|
|
|
1070
1202
|
return [dict(r) for r in regimes if isinstance(r, dict)]
|
|
1071
1203
|
|
|
1072
1204
|
|
|
1205
|
+
# #825/#832 spec 5.4 "Calibration sidecar". Recovery (#689) journals a
|
|
1206
|
+
# transition an earlier run persisted and failed to record. A transition a
|
|
1207
|
+
# previous-release process persisted must never be republished as a
|
|
1208
|
+
# current-stamped `meter_rate_change`, so every regime THIS binary writes
|
|
1209
|
+
# carries the writer generation plus a digest over its own content: a legacy
|
|
1210
|
+
# process's in-place `update()` of a stamped regime keeps the stamp keys but
|
|
1211
|
+
# breaks the digest, so it reads as legacy too.
|
|
1212
|
+
_REGIME_WRITER_KEY = "wpg"
|
|
1213
|
+
_REGIME_WRITER_DIGEST_KEY = "wpgDigest"
|
|
1214
|
+
|
|
1215
|
+
|
|
1216
|
+
def _regime_content_digest(regime: dict) -> str:
|
|
1217
|
+
core = {k: v for k, v in regime.items()
|
|
1218
|
+
if k not in (_REGIME_WRITER_KEY, _REGIME_WRITER_DIGEST_KEY)}
|
|
1219
|
+
data = json.dumps(core, sort_keys=True, separators=(",", ":"),
|
|
1220
|
+
ensure_ascii=False, default=str)
|
|
1221
|
+
return "sha256:" + hashlib.sha256(data.encode("utf-8")).hexdigest()
|
|
1222
|
+
|
|
1223
|
+
|
|
1224
|
+
def stamp_regime(regime: dict) -> dict:
|
|
1225
|
+
"""``regime`` stamped as written by the current protocol generation."""
|
|
1226
|
+
import _lib_account_protocol
|
|
1227
|
+
out = {k: v for k, v in regime.items()
|
|
1228
|
+
if k not in (_REGIME_WRITER_KEY, _REGIME_WRITER_DIGEST_KEY)}
|
|
1229
|
+
out[_REGIME_WRITER_KEY] = _lib_account_protocol.PROTOCOL_GENERATION
|
|
1230
|
+
out[_REGIME_WRITER_DIGEST_KEY] = _regime_content_digest(out)
|
|
1231
|
+
return out
|
|
1232
|
+
|
|
1233
|
+
|
|
1234
|
+
def regime_writer_is_current(regime) -> bool:
|
|
1235
|
+
import _lib_account_protocol
|
|
1236
|
+
return (isinstance(regime, dict)
|
|
1237
|
+
and regime.get(_REGIME_WRITER_KEY)
|
|
1238
|
+
== _lib_account_protocol.PROTOCOL_GENERATION
|
|
1239
|
+
and regime.get(_REGIME_WRITER_DIGEST_KEY)
|
|
1240
|
+
== _regime_content_digest(regime))
|
|
1241
|
+
|
|
1242
|
+
|
|
1243
|
+
def stamp_current_writes(stored: dict, reduced: dict, account_key) -> dict:
|
|
1244
|
+
"""``reduced`` with every regime this run created or changed stamped.
|
|
1245
|
+
|
|
1246
|
+
A regime identical to the stored one at its position is left exactly as
|
|
1247
|
+
it was — a legacy regime this run did not touch is never laundered into a
|
|
1248
|
+
current one."""
|
|
1249
|
+
before = stored_regimes(stored, account_key)
|
|
1250
|
+
after = stored_regimes(reduced, account_key)
|
|
1251
|
+
regimes = []
|
|
1252
|
+
for index, regime in enumerate(after):
|
|
1253
|
+
if index < len(before) and regime == before[index]:
|
|
1254
|
+
regimes.append(regime)
|
|
1255
|
+
else:
|
|
1256
|
+
regimes.append(stamp_regime(regime))
|
|
1257
|
+
out = dict(reduced)
|
|
1258
|
+
accounts = dict(out.get("accounts") or {})
|
|
1259
|
+
accounts[_state_key(account_key)] = {
|
|
1260
|
+
**(accounts.get(_state_key(account_key)) or {}), "regimes": regimes}
|
|
1261
|
+
out["accounts"] = accounts
|
|
1262
|
+
return out
|
|
1263
|
+
|
|
1264
|
+
|
|
1265
|
+
def recovery_candidates(state: dict, account_key, *, detected_at) -> tuple:
|
|
1266
|
+
"""The persisted transitions #689 recovery may journal: every qualified
|
|
1267
|
+
pair whose SUCCESSOR — the regime that constitutes the transition — carries
|
|
1268
|
+
a verifying current-writer stamp. A transition a previous-release process
|
|
1269
|
+
persisted (or edited in place) is withheld. The predecessor is ordinary
|
|
1270
|
+
history: every regime from before the upgrade is legacy-written, and the
|
|
1271
|
+
first current change after it must still be recoverable."""
|
|
1272
|
+
mrc = _cctally()._load_sibling("_lib_meter_rate_change")
|
|
1273
|
+
regimes = stored_regimes(state, account_key)
|
|
1274
|
+
current_starts = set()
|
|
1275
|
+
for start, (_predecessor, successor) in mrc._adjacent_pairs(regimes).items():
|
|
1276
|
+
if regime_writer_is_current(successor):
|
|
1277
|
+
current_starts.add(start)
|
|
1278
|
+
return tuple(
|
|
1279
|
+
t for t in mrc.enumerate_transitions(
|
|
1280
|
+
regimes, provider="claude",
|
|
1281
|
+
account_key=_alert_account_key(account_key),
|
|
1282
|
+
detected_at=detected_at)
|
|
1283
|
+
if mrc._instant(t.effective_from) in current_starts)
|
|
1284
|
+
|
|
1285
|
+
|
|
1073
1286
|
def recorded_regime(state: dict, account_key) -> "dict | None":
|
|
1074
1287
|
"""The open regime a run would present as the durable prior, or None."""
|
|
1075
1288
|
return _open_regime(stored_regimes(state, account_key))
|
|
@@ -1183,9 +1396,11 @@ def reduce_state(stored: dict, analysis, mode: PersistMode) -> dict:
|
|
|
1183
1396
|
split persists BOTH sides, so the baseline is created if it is absent.
|
|
1184
1397
|
* A trustworthy fit with no detected change updates the open regime in
|
|
1185
1398
|
place, or opens the first one.
|
|
1186
|
-
* A stored regime under a different fingerprint
|
|
1187
|
-
|
|
1188
|
-
value was true under its
|
|
1399
|
+
* A stored regime under a different fingerprint, or a different
|
|
1400
|
+
coefficient era (#978), is marked `stale` and a new regime is APPENDED;
|
|
1401
|
+
its own value is never rewritten, because that value was true under its
|
|
1402
|
+
own constants. A run whose clock selects an earlier era than the open
|
|
1403
|
+
regime's writes nothing.
|
|
1189
1404
|
* A withheld analysis that `qm.transition_persistence_permitted` admits
|
|
1190
1405
|
persists both sides of the split exactly as a confirmed change does,
|
|
1191
1406
|
with the successor stamped under the withholding status (#688). This
|
|
@@ -1211,14 +1426,29 @@ def reduce_state(stored: dict, analysis, mode: PersistMode) -> dict:
|
|
|
1211
1426
|
successor_ok = successor_ok or permitted
|
|
1212
1427
|
if not fitted_ok and not successor_ok:
|
|
1213
1428
|
return stored
|
|
1429
|
+
mode_era = qm.era_by_id(mode.coefficient_era)
|
|
1430
|
+
if mode_era is None:
|
|
1431
|
+
# No validated era: nothing this run fitted could name one.
|
|
1432
|
+
return stored
|
|
1214
1433
|
|
|
1215
1434
|
open_regime = _open_regime(regimes)
|
|
1216
|
-
#
|
|
1217
|
-
#
|
|
1218
|
-
#
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1435
|
+
# #978 spec 4.5: a run whose clock selects an EARLIER era than the open
|
|
1436
|
+
# regime's — a `CCTALLY_AS_OF` inside the Opus 5 era against a stored
|
|
1437
|
+
# Opus 5.5 calibration — never replaces the present with the past.
|
|
1438
|
+
open_era = (qm.era_by_id(open_regime.get("coefficientEra"))
|
|
1439
|
+
if open_regime is not None else None)
|
|
1440
|
+
if open_era is not None and open_era.start > mode_era.start:
|
|
1441
|
+
return stored
|
|
1442
|
+
# A stored regime fitted under different constants, or in a different
|
|
1443
|
+
# coefficient era (#978), is marked stale and left otherwise untouched.
|
|
1444
|
+
# It is marked HERE, together with the append below, because section 20
|
|
1445
|
+
# also says a run that writes nothing leaves the stored state alone — and
|
|
1446
|
+
# a run reaching this point is writing. The era comparison uses `.get()`,
|
|
1447
|
+
# so an old-build regime with no era rolls over here too (its fingerprint
|
|
1448
|
+
# already differs).
|
|
1449
|
+
if open_regime is not None and (
|
|
1450
|
+
open_regime.get("fingerprint") != mode.fingerprint
|
|
1451
|
+
or open_regime.get("coefficientEra") != mode.coefficient_era):
|
|
1222
1452
|
open_regime["status"] = CalibrationStatus.STALE.value
|
|
1223
1453
|
open_regime["effectiveUntil"] = mode.now.astimezone(UTC).isoformat()
|
|
1224
1454
|
open_regime = None
|
|
@@ -1360,6 +1590,8 @@ def persist_and_detect(analysis, mode: PersistMode) -> tuple:
|
|
|
1360
1590
|
detected_at = mode.now.astimezone(UTC).isoformat()
|
|
1361
1591
|
transitions: tuple = ()
|
|
1362
1592
|
if reduced is not loaded.state:
|
|
1593
|
+
reduced = stamp_current_writes(
|
|
1594
|
+
loaded.state, reduced, mode.account_key)
|
|
1363
1595
|
save_calibrations(reduced)
|
|
1364
1596
|
# #688: `detect_transitions` compares two stored states and does
|
|
1365
1597
|
# not know WHY this run was admitted, so the disclosure is
|
|
@@ -1393,10 +1625,8 @@ def persist_and_detect(analysis, mode: PersistMode) -> tuple:
|
|
|
1393
1625
|
# later run finds the state unchanged and takes neither branch above.
|
|
1394
1626
|
# Recovery therefore cannot be derived from freshness; it comes from
|
|
1395
1627
|
# what is persisted right now.
|
|
1396
|
-
candidates =
|
|
1397
|
-
|
|
1398
|
-
provider="claude", account_key=account_key,
|
|
1399
|
-
detected_at=detected_at)
|
|
1628
|
+
candidates = recovery_candidates(
|
|
1629
|
+
reduced, mode.account_key, detected_at=detected_at)
|
|
1400
1630
|
return loaded.quarantined, transitions, candidates
|
|
1401
1631
|
|
|
1402
1632
|
|
|
@@ -1760,17 +1990,45 @@ class AccountResult:
|
|
|
1760
1990
|
#: being accepted; `analysis` is what the user is shown.
|
|
1761
1991
|
clean: object = None
|
|
1762
1992
|
quarantined: "str | None" = None
|
|
1993
|
+
#: The coefficient era the clock selected, or None before every
|
|
1994
|
+
#: validated era (#978). The method metadata names it.
|
|
1995
|
+
era: "qm.CoefficientEra | None" = None
|
|
1996
|
+
|
|
1997
|
+
|
|
1998
|
+
def recorded_prior_is_stale(recorded, era) -> bool:
|
|
1999
|
+
"""Is the recorded calibration from other constants or another era?
|
|
2000
|
+
|
|
2001
|
+
Spec 4.3's status table: either one is the `stale` overlay, as a
|
|
2002
|
+
fingerprint mismatch already was. A run with no selected era compares
|
|
2003
|
+
against no era id, so any recorded regime is stale to it.
|
|
2004
|
+
"""
|
|
2005
|
+
if recorded is None:
|
|
2006
|
+
return False
|
|
2007
|
+
if recorded.get("fingerprint") != qm.QUOTA_MODEL_CONSTANTS_FINGERPRINT:
|
|
2008
|
+
return True
|
|
2009
|
+
return recorded.get("coefficientEra") != (
|
|
2010
|
+
None if era is None else era.id)
|
|
1763
2011
|
|
|
1764
2012
|
|
|
1765
2013
|
def analyse_account(account_key, *, now, since, watch_from,
|
|
1766
2014
|
stored_state) -> AccountResult:
|
|
1767
2015
|
"""Read one account's population and run the kernel over it."""
|
|
1768
|
-
|
|
2016
|
+
# #978 spec 4.3: ONE era per analysis, chosen once from the clock. The
|
|
2017
|
+
# population is confined to it, so every kernel call below sees only
|
|
2018
|
+
# in-era instants and weighs them with this era's coefficients.
|
|
2019
|
+
era = select_era(now)
|
|
2020
|
+
load = load_population(account_key, since=since, now=now, era=era)
|
|
1769
2021
|
extra = [load.status] if load.status is not None else []
|
|
2022
|
+
# A clock before every validated era read nothing, so the population is
|
|
2023
|
+
# empty and no weight is ever applied. The kernel still requires an era
|
|
2024
|
+
# to type its arguments; the earliest one is an inert placeholder here.
|
|
2025
|
+
kernel_era = era if era is not None else qm.COEFFICIENT_ERAS[0]
|
|
1770
2026
|
segments = qm.build_segments(load.snapshots, load.credits)
|
|
1771
2027
|
series = qm.build_daily_series(
|
|
1772
|
-
segments, load.entries, now=now, newest_entry_at=load.newest_entry_at
|
|
2028
|
+
segments, load.entries, now=now, newest_entry_at=load.newest_entry_at,
|
|
2029
|
+
era=kernel_era)
|
|
1773
2030
|
week = current_week_window(segments, now=now)
|
|
2031
|
+
week_withheld = None
|
|
1774
2032
|
if week is None:
|
|
1775
2033
|
forecast_population: tuple = ()
|
|
1776
2034
|
week_units = None
|
|
@@ -1779,24 +2037,22 @@ def analyse_account(account_key, *, now, since, watch_from,
|
|
|
1779
2037
|
else:
|
|
1780
2038
|
horizon = min(now, week.end)
|
|
1781
2039
|
forecast_population = tuple(
|
|
1782
|
-
e for e in load.entries if week.units_start <= e.at < horizon
|
|
1783
|
-
|
|
1784
|
-
for entry in forecast_population:
|
|
1785
|
-
units = qm.weighted_units(entry)
|
|
1786
|
-
if units is None:
|
|
1787
|
-
continue
|
|
1788
|
-
if qm.family_participation(
|
|
1789
|
-
qm.normalize_family(entry.model)) != "general":
|
|
1790
|
-
continue
|
|
1791
|
-
week_units += units
|
|
2040
|
+
e for e in load.entries if week.units_start <= e.at < horizon
|
|
2041
|
+
and qm.era_contains(kernel_era, e.at))
|
|
1792
2042
|
observed = week.observed_percent
|
|
1793
2043
|
week_start, week_end = week.units_start, week.end
|
|
2044
|
+
if week_spans_era(week, kernel_era):
|
|
2045
|
+
# The week's units would come from two eras, and the in-era part
|
|
2046
|
+
# is not the whole week, so consumption, the residual, headroom
|
|
2047
|
+
# and the projection are withheld. The observed reading and an
|
|
2048
|
+
# independently valid fit are still published.
|
|
2049
|
+
week_withheld = "unvalidated-coefficient-era"
|
|
2050
|
+
week_units = None
|
|
2051
|
+
else:
|
|
2052
|
+
week_units = current_week_units(forecast_population, kernel_era)
|
|
1794
2053
|
|
|
1795
2054
|
recorded = recorded_regime(stored_state, account_key)
|
|
1796
|
-
stale_prior =
|
|
1797
|
-
recorded is not None
|
|
1798
|
-
and recorded.get("fingerprint")
|
|
1799
|
-
!= qm.QUOTA_MODEL_CONSTANTS_FINGERPRINT)
|
|
2055
|
+
stale_prior = recorded_prior_is_stale(recorded, era)
|
|
1800
2056
|
mode_kind = "automatic"
|
|
1801
2057
|
if watch_from is not None:
|
|
1802
2058
|
mode_kind = "diagnostic"
|
|
@@ -1806,13 +2062,15 @@ def analyse_account(account_key, *, now, since, watch_from,
|
|
|
1806
2062
|
mode_kind = "diagnostic"
|
|
1807
2063
|
|
|
1808
2064
|
common = dict(
|
|
1809
|
-
now=now, newest_at=load.newest_entry_at,
|
|
2065
|
+
now=now, newest_at=load.newest_entry_at, era=kernel_era,
|
|
2066
|
+
current_week_withheld=week_withheld,
|
|
1810
2067
|
forecast_population=forecast_population,
|
|
1811
2068
|
observed_percent=observed, current_week_units=week_units,
|
|
1812
2069
|
current_week_start=week_start, current_week_end=week_end,
|
|
1813
2070
|
override_split=watch_from.date() if watch_from is not None else None,
|
|
1814
2071
|
extra_statuses=tuple(extra),
|
|
1815
|
-
unattributed=qm.unattributed_units(segments, load.entries, now=now
|
|
2072
|
+
unattributed=qm.unattributed_units(segments, load.entries, now=now,
|
|
2073
|
+
era=kernel_era),
|
|
1816
2074
|
in_progress_excluded=qm.in_progress_dates(segments, now=now),
|
|
1817
2075
|
diagnostics={"snapshots": len(load.snapshots),
|
|
1818
2076
|
"segments": len(segments),
|
|
@@ -1833,7 +2091,7 @@ def analyse_account(account_key, *, now, since, watch_from,
|
|
|
1833
2091
|
account_key=account_key,
|
|
1834
2092
|
label=account_display_label(account_key),
|
|
1835
2093
|
analysis=reported, load=load, week=week, recorded=recorded,
|
|
1836
|
-
stale_prior=stale_prior, mode_kind=mode_kind, clean=clean)
|
|
2094
|
+
stale_prior=stale_prior, mode_kind=mode_kind, clean=clean, era=era)
|
|
1837
2095
|
|
|
1838
2096
|
|
|
1839
2097
|
# ---------------------------------------------------------------------------
|
|
@@ -1892,6 +2150,10 @@ def account_payload(result: AccountResult) -> dict:
|
|
|
1892
2150
|
# any caller could omit, and section 24 requires it published.
|
|
1893
2151
|
diagnostics.pop("detector", None)
|
|
1894
2152
|
detector = qm.detector_diagnostics(analysis.detector)
|
|
2153
|
+
era = result.era
|
|
2154
|
+
supported_from = None if era is None else era.start.isoformat()
|
|
2155
|
+
supported_until = (None if era is None or era.end is None
|
|
2156
|
+
else era.end.isoformat())
|
|
1895
2157
|
return {
|
|
1896
2158
|
"status": analysis.status.value,
|
|
1897
2159
|
"verdict": analysis.verdict.value,
|
|
@@ -1958,9 +2220,14 @@ def account_payload(result: AccountResult) -> dict:
|
|
|
1958
2220
|
"algorithmRevision": qm.QUOTA_MODEL_ALGORITHM_REVISION,
|
|
1959
2221
|
"constantsFingerprint": qm.QUOTA_MODEL_CONSTANTS_FINGERPRINT,
|
|
1960
2222
|
"verifiedAt": qm.QUOTA_MODEL_VERIFIED_AT,
|
|
1961
|
-
"supportedCompositionFrom":
|
|
1962
|
-
"coefficientSupport":
|
|
1963
|
-
|
|
2223
|
+
"supportedCompositionFrom": supported_from,
|
|
2224
|
+
"coefficientSupport": {
|
|
2225
|
+
"supportedCompositionFrom": supported_from,
|
|
2226
|
+
"supportedCompositionUntil": supported_until,
|
|
2227
|
+
"coefficientEra": None if era is None else era.id,
|
|
2228
|
+
"fastModeParticipation": qm.FAST_MODE_PARTICIPATION,
|
|
2229
|
+
},
|
|
2230
|
+
"coefficientEras": published_coefficient_eras(),
|
|
1964
2231
|
"detector": detector,
|
|
1965
2232
|
"dedicatedPoolScope": qm.DEDICATED_POOL_SCOPE_NOTE,
|
|
1966
2233
|
},
|
|
@@ -2018,6 +2285,15 @@ def _evidence_line(label, evidence, formatter) -> str:
|
|
|
2018
2285
|
return f" {label:26} {formatter(evidence.value)}{span}{marks}"
|
|
2019
2286
|
|
|
2020
2287
|
|
|
2288
|
+
def _supported_composition_text(era) -> str:
|
|
2289
|
+
"""The selected era's interval, as the DATA line names it (#978)."""
|
|
2290
|
+
if era is None:
|
|
2291
|
+
return f"none validated before {SUPPORTED_COMPOSITION_FROM.isoformat()}"
|
|
2292
|
+
if era.end is None:
|
|
2293
|
+
return f"{era.start.isoformat()} onward"
|
|
2294
|
+
return f"{era.start.isoformat()} to {era.end.isoformat()}"
|
|
2295
|
+
|
|
2296
|
+
|
|
2021
2297
|
def render_text(results, *, now, decorated) -> list:
|
|
2022
2298
|
"""The human report, as a list of lines."""
|
|
2023
2299
|
out: list = []
|
|
@@ -2032,9 +2308,9 @@ def render_text(results, *, now, decorated) -> list:
|
|
|
2032
2308
|
out.append(f" entries through "
|
|
2033
2309
|
f"{_iso(result.load.newest_entry_at) or '-'}")
|
|
2034
2310
|
out.append(f" analysis start "
|
|
2035
|
-
f"{_iso(result.load.analysis_start)}"
|
|
2311
|
+
f"{_iso(result.load.analysis_start) or '-'}"
|
|
2036
2312
|
f" (supported composition: "
|
|
2037
|
-
f"{
|
|
2313
|
+
f"{_supported_composition_text(result.era)})")
|
|
2038
2314
|
out.append(f" eligible days "
|
|
2039
2315
|
f"{analysis.diagnostics.get('eligibleDays', 0)}"
|
|
2040
2316
|
f" ({analysis.diagnostics.get('withheldDays', 0)} withheld)")
|
|
@@ -2215,7 +2491,8 @@ def cmd_quota(args) -> int:
|
|
|
2215
2491
|
mode = PersistMode(
|
|
2216
2492
|
kind=result.mode_kind, account_key=account_key, now=now,
|
|
2217
2493
|
fingerprint=qm.QUOTA_MODEL_CONSTANTS_FINGERPRINT,
|
|
2218
|
-
regime_start=result.load.analysis_start
|
|
2494
|
+
regime_start=result.load.analysis_start,
|
|
2495
|
+
coefficient_era=None if result.era is None else result.era.id)
|
|
2219
2496
|
# §6.5 step 1-2: the descriptor is decided under the calibration
|
|
2220
2497
|
# file's leaf lock, and that lock is released before any stats lock is
|
|
2221
2498
|
# taken — `persist_and_detect` returns from its `with` block first.
|