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
package/bin/_lib_quota_model.py
CHANGED
|
@@ -27,6 +27,7 @@ import json
|
|
|
27
27
|
import math
|
|
28
28
|
import re
|
|
29
29
|
import statistics
|
|
30
|
+
import typing
|
|
30
31
|
|
|
31
32
|
from _lib_pricing import _strip_anthropic_model_prefix
|
|
32
33
|
|
|
@@ -36,14 +37,9 @@ from _lib_pricing import _strip_anthropic_model_prefix
|
|
|
36
37
|
# fitted under a different fingerprint is `stale`.
|
|
37
38
|
# ---------------------------------------------------------------------------
|
|
38
39
|
|
|
39
|
-
#:
|
|
40
|
-
#:
|
|
41
|
-
|
|
42
|
-
"fresh": 1.0,
|
|
43
|
-
"output": 4.73,
|
|
44
|
-
"cache_1h": 1.03,
|
|
45
|
-
"cache_read": 0.0031,
|
|
46
|
-
}
|
|
40
|
+
#: The four weighted token classes, in the order every era states its
|
|
41
|
+
#: weights. The five-minute cache-write quantity is weighted as fresh input.
|
|
42
|
+
TOKEN_CLASSES: tuple[str, ...] = ("fresh", "output", "cache_1h", "cache_read")
|
|
47
43
|
|
|
48
44
|
#: Recognized model spellings (case-folded, provider prefix stripped, anchored
|
|
49
45
|
#: terminal -YYYYMMDD removed) resolved to their canonical family. An
|
|
@@ -87,6 +83,12 @@ FAMILY_ALIASES: dict[str, str] = {
|
|
|
87
83
|
"claude-mythos-5": "claude-mythos-5",
|
|
88
84
|
"claude-mythos-5-1": "claude-mythos-5-1",
|
|
89
85
|
"claude-mythos-preview": "claude-mythos-5",
|
|
86
|
+
# The 5.5 generation (#978). Each is its OWN canonical family, on the
|
|
87
|
+
# #704 precedent: pooling one into the family it follows would assert
|
|
88
|
+
# equal per-unit consumption, which nothing here establishes.
|
|
89
|
+
"claude-opus-5-5": "claude-opus-5-5",
|
|
90
|
+
"claude-sonnet-5-5": "claude-sonnet-5-5",
|
|
91
|
+
"claude-haiku-5-5": "claude-haiku-5-5",
|
|
90
92
|
}
|
|
91
93
|
|
|
92
94
|
#: Whether a canonical family drains the general weekly quota, drains a
|
|
@@ -114,6 +116,9 @@ FAMILY_PARTICIPATION: dict[str, str] = {
|
|
|
114
116
|
"claude-fable-5-1": "general",
|
|
115
117
|
"claude-mythos-5": "general",
|
|
116
118
|
"claude-mythos-5-1": "general",
|
|
119
|
+
"claude-opus-5-5": "general",
|
|
120
|
+
"claude-sonnet-5-5": "general",
|
|
121
|
+
"claude-haiku-5-5": "general",
|
|
117
122
|
}
|
|
118
123
|
|
|
119
124
|
#: Where each classification came from, so a later reader can re-test it
|
|
@@ -121,6 +126,12 @@ FAMILY_PARTICIPATION: dict[str, str] = {
|
|
|
121
126
|
#: operator-supplied and are NOT derivable from this repository. No family is
|
|
122
127
|
#: ever classified "unknown" on the strength of unfamiliarity with it: an
|
|
123
128
|
#: unrecognized family is escalated to the operator instead.
|
|
129
|
+
_PROVENANCE_55: str = (
|
|
130
|
+
"operator-supplied in #978 (2026-10-18), confirmed by the 2026-10-20 "
|
|
131
|
+
"handoff: participates in the general weekly meter; modelled with the "
|
|
132
|
+
"selected coefficient era's shared token-class coefficients"
|
|
133
|
+
)
|
|
134
|
+
|
|
124
135
|
FAMILY_PROVENANCE: dict[str, str] = {
|
|
125
136
|
"claude-fable-5": (
|
|
126
137
|
"operator-supplied 2026-08-28: drains a dedicated pool AND the "
|
|
@@ -143,6 +154,10 @@ FAMILY_PROVENANCE: dict[str, str] = {
|
|
|
143
154
|
"general weekly meter on the same terms as claude-mythos-5; only "
|
|
144
155
|
"the general draining is modelled"
|
|
145
156
|
),
|
|
157
|
+
# Participation is the operator's statement, never the API price card.
|
|
158
|
+
"claude-opus-5-5": _PROVENANCE_55,
|
|
159
|
+
"claude-sonnet-5-5": _PROVENANCE_55,
|
|
160
|
+
"claude-haiku-5-5": _PROVENANCE_55,
|
|
146
161
|
}
|
|
147
162
|
|
|
148
163
|
#: Eligibility fence parameters (section 5).
|
|
@@ -193,14 +208,70 @@ MIX_SUPPORT: dict[str, object] = {
|
|
|
193
208
|
"token_class_max_relative_effect": 0.05,
|
|
194
209
|
}
|
|
195
210
|
|
|
196
|
-
#:
|
|
197
|
-
#:
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
211
|
+
#: The fast-mode policy every validated era applies: fast-mode requests are
|
|
212
|
+
#: billed to usage credits only and never reach the weekly meter.
|
|
213
|
+
FAST_MODE_PARTICIPATION: str = "usage-credits-only-excluded"
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@dataclasses.dataclass(frozen=True)
|
|
217
|
+
class CoefficientEra:
|
|
218
|
+
"""One validated coefficient era: a half-open UTC date interval, the four
|
|
219
|
+
token-class weights fitted for it, its support policy and provenance.
|
|
220
|
+
|
|
221
|
+
`start` is inclusive and `end` exclusive, both at UTC midnight; an open
|
|
222
|
+
era has `end=None`. `weights` is a tuple of `(class, weight)` pairs in
|
|
223
|
+
`TOKEN_CLASSES` order, so the record stays immutable and hashable.
|
|
224
|
+
"""
|
|
225
|
+
|
|
226
|
+
id: str
|
|
227
|
+
start: "dt.date"
|
|
228
|
+
end: "dt.date | None"
|
|
229
|
+
weights: tuple
|
|
230
|
+
fast_mode_participation: str
|
|
231
|
+
provenance: str
|
|
232
|
+
|
|
233
|
+
def weight_map(self) -> dict[str, float]:
|
|
234
|
+
"""The weights as a fresh `{class: weight}` dict the caller may own."""
|
|
235
|
+
return {name: float(weight) for name, weight in self.weights}
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
#: The ordered, immutable table of validated coefficient eras (spec 4.2).
|
|
239
|
+
#: Exactly one era covers any instant from 2026-07-25 on; earlier history is
|
|
240
|
+
#: covered by none and stays unvalidated. There is deliberately no mutable
|
|
241
|
+
#: "current weights" global: every weighting path asks this table.
|
|
242
|
+
COEFFICIENT_ERAS: tuple = (
|
|
243
|
+
CoefficientEra(
|
|
244
|
+
id="opus-5",
|
|
245
|
+
start=dt.date(2026, 7, 25),
|
|
246
|
+
end=dt.date(2026, 9, 22),
|
|
247
|
+
weights=(("fresh", 1.0), ("output", 4.73), ("cache_1h", 1.03),
|
|
248
|
+
("cache_read", 0.0031)),
|
|
249
|
+
fast_mode_participation=FAST_MODE_PARTICIPATION,
|
|
250
|
+
provenance='The Opus 5 calibration, docs/quota-model.md "The finding"',
|
|
251
|
+
),
|
|
252
|
+
CoefficientEra(
|
|
253
|
+
id="opus-5-5",
|
|
254
|
+
start=dt.date(2026, 9, 22),
|
|
255
|
+
end=None,
|
|
256
|
+
weights=(("fresh", 1.0), ("output", 0.64), ("cache_1h", 1.0),
|
|
257
|
+
("cache_read", 0.0148)),
|
|
258
|
+
fast_mode_participation=FAST_MODE_PARTICIPATION,
|
|
259
|
+
provenance=("The 2026-10-18 Opus 5.5 refit; output weakly identified "
|
|
260
|
+
"(95% CI 0.23\u20131.13), cache read sharp "
|
|
261
|
+
"(0.0128\u20130.0168)"),
|
|
262
|
+
),
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _era_payload(era: CoefficientEra) -> dict:
|
|
267
|
+
return {
|
|
268
|
+
"id": era.id,
|
|
269
|
+
"from": era.start.isoformat(),
|
|
270
|
+
"until": None if era.end is None else era.end.isoformat(),
|
|
271
|
+
"tokenClassWeights": era.weight_map(),
|
|
272
|
+
"fastModeParticipation": era.fast_mode_participation,
|
|
273
|
+
"provenance": era.provenance,
|
|
274
|
+
}
|
|
204
275
|
|
|
205
276
|
#: The reading-to-interval rule of section 4. Bumped when that rule changes.
|
|
206
277
|
DISPLAY_RULE_REVISION: int = 1
|
|
@@ -229,8 +300,8 @@ DEDICATED_POOL_SCOPE_NOTE: str = (
|
|
|
229
300
|
"only general-quota draining is modelled"
|
|
230
301
|
)
|
|
231
302
|
|
|
232
|
-
QUOTA_MODEL_ALGORITHM_REVISION: int =
|
|
233
|
-
QUOTA_MODEL_VERIFIED_AT: str = "2026-
|
|
303
|
+
QUOTA_MODEL_ALGORITHM_REVISION: int = 4
|
|
304
|
+
QUOTA_MODEL_VERIFIED_AT: str = "2026-10-20"
|
|
234
305
|
|
|
235
306
|
|
|
236
307
|
def constants_payload() -> dict:
|
|
@@ -244,7 +315,6 @@ def constants_payload() -> dict:
|
|
|
244
315
|
"algorithmRevision": QUOTA_MODEL_ALGORITHM_REVISION,
|
|
245
316
|
"verifiedAt": QUOTA_MODEL_VERIFIED_AT,
|
|
246
317
|
"displayRuleRevision": DISPLAY_RULE_REVISION,
|
|
247
|
-
"tokenClassWeights": dict(TOKEN_CLASS_WEIGHTS),
|
|
248
318
|
"familyAliases": dict(FAMILY_ALIASES),
|
|
249
319
|
"familyParticipation": dict(FAMILY_PARTICIPATION),
|
|
250
320
|
"familyProvenance": dict(FAMILY_PROVENANCE),
|
|
@@ -252,7 +322,7 @@ def constants_payload() -> dict:
|
|
|
252
322
|
"detector": dict(DETECTOR),
|
|
253
323
|
"trust": dict(TRUST),
|
|
254
324
|
"mixSupport": dict(MIX_SUPPORT),
|
|
255
|
-
"
|
|
325
|
+
"coefficientEras": [_era_payload(era) for era in COEFFICIENT_ERAS],
|
|
256
326
|
}
|
|
257
327
|
|
|
258
328
|
|
|
@@ -268,7 +338,7 @@ def constants_fingerprint() -> str:
|
|
|
268
338
|
#: constant above without re-pinning this line is caught rather than silently
|
|
269
339
|
#: invalidating every persisted calibration.
|
|
270
340
|
QUOTA_MODEL_CONSTANTS_FINGERPRINT: str = (
|
|
271
|
-
"
|
|
341
|
+
"5835b430978b2fc556c577d44c9e62d5d7807e62533b32819ed63d37e3af671a"
|
|
272
342
|
)
|
|
273
343
|
|
|
274
344
|
|
|
@@ -735,12 +805,136 @@ def token_class_units(e: EntryRecord) -> dict[str, float] | None:
|
|
|
735
805
|
}
|
|
736
806
|
|
|
737
807
|
|
|
738
|
-
|
|
739
|
-
|
|
808
|
+
# ---------------------------------------------------------------------------
|
|
809
|
+
# Coefficient eras (#978, spec section 4.2).
|
|
810
|
+
#
|
|
811
|
+
# One covering era per instant, selected by the instant alone and never by
|
|
812
|
+
# the model name. The raw decomposition above is era-independent; everything
|
|
813
|
+
# that turns raw class quantities into units goes through `class_units`.
|
|
814
|
+
# ---------------------------------------------------------------------------
|
|
815
|
+
#: `weigh_entry`'s cause when no validated era covers the entry, or the
|
|
816
|
+
#: explicitly requested era does not. A member of `EVIDENCE_CODES`.
|
|
817
|
+
ERA_UNAVAILABLE: str = "unvalidated-coefficient-era"
|
|
818
|
+
|
|
819
|
+
#: `weigh_entry`'s cause when `token_class_units` cannot split the entry.
|
|
820
|
+
SPLIT_UNKNOWN: str = "token-split-unknown"
|
|
821
|
+
|
|
822
|
+
|
|
823
|
+
class CoefficientEraMismatch(ValueError):
|
|
824
|
+
"""A population-level operation was handed an instant outside its era.
|
|
825
|
+
|
|
826
|
+
Units from two eras are never added together or divided by one budget
|
|
827
|
+
(spec section 4.2, invariant 5), so the refusal is loud rather than a
|
|
828
|
+
silent reweighting.
|
|
829
|
+
"""
|
|
830
|
+
|
|
831
|
+
|
|
832
|
+
def _utc_midnight(date) -> "dt.datetime":
|
|
833
|
+
return dt.datetime.combine(date, dt.time(0, 0), tzinfo=dt.timezone.utc)
|
|
834
|
+
|
|
835
|
+
|
|
836
|
+
def era_bounds(era: CoefficientEra) -> tuple:
|
|
837
|
+
"""`(start, end)` as aware UTC-midnight instants; `end` None when open."""
|
|
838
|
+
return (_utc_midnight(era.start),
|
|
839
|
+
None if era.end is None else _utc_midnight(era.end))
|
|
840
|
+
|
|
841
|
+
|
|
842
|
+
def era_contains(era: CoefficientEra, at: "dt.datetime") -> bool:
|
|
843
|
+
"""`start <= at < end`, the half-open interval, for an aware instant."""
|
|
844
|
+
require_aware(at, "at")
|
|
845
|
+
start, end = era_bounds(era)
|
|
846
|
+
return at >= start and (end is None or at < end)
|
|
847
|
+
|
|
848
|
+
|
|
849
|
+
def era_for_instant(at: "dt.datetime") -> "CoefficientEra | None":
|
|
850
|
+
"""The one validated era covering `at`, or None before 2026-07-25.
|
|
851
|
+
|
|
852
|
+
Raises on a naive datetime: a host-local reading would move the boundary
|
|
853
|
+
by the process's UTC offset.
|
|
854
|
+
"""
|
|
855
|
+
require_aware(at, "at")
|
|
856
|
+
for era in COEFFICIENT_ERAS:
|
|
857
|
+
if era_contains(era, at):
|
|
858
|
+
return era
|
|
859
|
+
return None
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
def era_by_id(era_id: object) -> "CoefficientEra | None":
|
|
863
|
+
"""The era whose identifier is exactly `era_id`, or None.
|
|
864
|
+
|
|
865
|
+
Anything that is not a known identifier string — None, a number, an
|
|
866
|
+
unknown name — is None, which a stored-regime reader treats as
|
|
867
|
+
malformed.
|
|
868
|
+
"""
|
|
869
|
+
if not isinstance(era_id, str):
|
|
870
|
+
return None
|
|
871
|
+
for era in COEFFICIENT_ERAS:
|
|
872
|
+
if era.id == era_id:
|
|
873
|
+
return era
|
|
874
|
+
return None
|
|
875
|
+
|
|
876
|
+
|
|
877
|
+
def class_units(classes, era: CoefficientEra) -> float:
|
|
878
|
+
"""Raw class quantities weighted by `era`'s coefficients.
|
|
879
|
+
|
|
880
|
+
The ONLY place in this module a quantity is multiplied by a weight, so
|
|
881
|
+
every aggregation path weights with the era it was given and none can keep
|
|
882
|
+
a multiplication of its own.
|
|
883
|
+
"""
|
|
884
|
+
weights = era.weight_map()
|
|
885
|
+
return sum(float(classes.get(name, 0.0)) * weights[name]
|
|
886
|
+
for name in TOKEN_CLASSES)
|
|
887
|
+
|
|
888
|
+
|
|
889
|
+
class EntryWeighting(typing.NamedTuple):
|
|
890
|
+
"""One entry's weighted units, or None with the typed reason why."""
|
|
891
|
+
|
|
892
|
+
units: "float | None"
|
|
893
|
+
cause: "str | None"
|
|
894
|
+
|
|
895
|
+
|
|
896
|
+
def weigh_entry(e: EntryRecord,
|
|
897
|
+
era: "CoefficientEra | None" = None) -> EntryWeighting:
|
|
898
|
+
"""Weighted units for one entry under its covering era.
|
|
899
|
+
|
|
900
|
+
With `era=None` the era is the one covering the entry's instant. An
|
|
901
|
+
instant no validated era covers, or an explicit `era` that does not
|
|
902
|
+
contain it, is `ERA_UNAVAILABLE` — checked FIRST, so an uncovered entry is
|
|
903
|
+
never reported as a split problem. An unknown split is `SPLIT_UNKNOWN`.
|
|
904
|
+
"""
|
|
905
|
+
covering = era_for_instant(e.at) if era is None else (
|
|
906
|
+
era if era_contains(era, e.at) else None)
|
|
907
|
+
if covering is None:
|
|
908
|
+
return EntryWeighting(None, ERA_UNAVAILABLE)
|
|
740
909
|
classes = token_class_units(e)
|
|
741
910
|
if classes is None:
|
|
742
|
-
return None
|
|
743
|
-
return
|
|
911
|
+
return EntryWeighting(None, SPLIT_UNKNOWN)
|
|
912
|
+
return EntryWeighting(class_units(classes, covering), None)
|
|
913
|
+
|
|
914
|
+
|
|
915
|
+
def weighted_units(e: EntryRecord,
|
|
916
|
+
era: "CoefficientEra | None" = None) -> float | None:
|
|
917
|
+
"""Weighted quota units for one entry, or None when it cannot be weighted.
|
|
918
|
+
|
|
919
|
+
None covers both an unknown split and an instant outside every validated
|
|
920
|
+
era (or outside `era`); `weigh_entry` says which.
|
|
921
|
+
"""
|
|
922
|
+
return weigh_entry(e, era).units
|
|
923
|
+
|
|
924
|
+
|
|
925
|
+
def _require_era(era) -> CoefficientEra:
|
|
926
|
+
if not isinstance(era, CoefficientEra):
|
|
927
|
+
raise TypeError(f"era must be a CoefficientEra, got {era!r}")
|
|
928
|
+
return era
|
|
929
|
+
|
|
930
|
+
|
|
931
|
+
def _require_in_era(era: CoefficientEra, instants, label: str) -> None:
|
|
932
|
+
"""Raise `CoefficientEraMismatch` on the first instant outside `era`."""
|
|
933
|
+
for at in instants:
|
|
934
|
+
if not era_contains(era, at):
|
|
935
|
+
raise CoefficientEraMismatch(
|
|
936
|
+
f"{label} {at.isoformat()} lies outside coefficient era "
|
|
937
|
+
f"{era.id!r}")
|
|
744
938
|
|
|
745
939
|
|
|
746
940
|
def integer_percent(value: float) -> int:
|
|
@@ -1055,8 +1249,8 @@ def _day_slices(segments, now):
|
|
|
1055
1249
|
)
|
|
1056
1250
|
|
|
1057
1251
|
|
|
1058
|
-
def build_daily_series(segments, entries, *, now,
|
|
1059
|
-
|
|
1252
|
+
def build_daily_series(segments, entries, *, now, newest_entry_at,
|
|
1253
|
+
era: CoefficientEra) -> list[DailyObservation]:
|
|
1060
1254
|
"""One observation per complete UTC date, in date order.
|
|
1061
1255
|
|
|
1062
1256
|
Within one day and one segment the observation runs from the first to the
|
|
@@ -1082,12 +1276,21 @@ def build_daily_series(segments, entries, *, now,
|
|
|
1082
1276
|
unfinished, so reporting it as `no-local-history` asserts something false
|
|
1083
1277
|
and, because it always falls on or after any split date, withheld every
|
|
1084
1278
|
verdict at exit 3. `in_progress_dates` reports it as a diagnostic instead.
|
|
1279
|
+
|
|
1280
|
+
`era` is the ONE coefficient era the whole series is weighted with
|
|
1281
|
+
(#978). Every entry and every segment reading must lie inside it, and a
|
|
1282
|
+
population that does not raises `CoefficientEraMismatch` rather than
|
|
1283
|
+
summing units from two eras.
|
|
1085
1284
|
"""
|
|
1086
1285
|
require_aware(now, "now")
|
|
1087
1286
|
if newest_entry_at is not None:
|
|
1088
1287
|
require_aware(newest_entry_at, "newest_entry_at")
|
|
1089
1288
|
for entry in entries:
|
|
1090
1289
|
require_aware(entry.at, "entry.at")
|
|
1290
|
+
_require_era(era)
|
|
1291
|
+
_require_in_era(era, (e.at for e in entries), "entry.at")
|
|
1292
|
+
_require_in_era(era, (r.at for seg in segments for r in seg.rows),
|
|
1293
|
+
"snapshot.at")
|
|
1091
1294
|
ordered = sorted(entries, key=lambda e: e.at)
|
|
1092
1295
|
instants = [e.at for e in ordered]
|
|
1093
1296
|
|
|
@@ -1122,7 +1325,7 @@ def build_daily_series(segments, entries, *, now,
|
|
|
1122
1325
|
# token count per class, which is what the composition support vector
|
|
1123
1326
|
# is (section 34) — a coefficient-weighted share cannot carry the
|
|
1124
1327
|
# materiality floor derived from those same coefficients.
|
|
1125
|
-
raw = {name: 0.0 for name in
|
|
1328
|
+
raw = {name: 0.0 for name in TOKEN_CLASSES}
|
|
1126
1329
|
units = 0.0
|
|
1127
1330
|
families: dict = {}
|
|
1128
1331
|
split_unknown = False
|
|
@@ -1136,10 +1339,9 @@ def build_daily_series(segments, entries, *, now,
|
|
|
1136
1339
|
if quantities is None:
|
|
1137
1340
|
split_unknown = True
|
|
1138
1341
|
continue
|
|
1139
|
-
entry_units = 0.0
|
|
1140
1342
|
for name, qty in quantities.items():
|
|
1141
1343
|
raw[name] += qty
|
|
1142
|
-
|
|
1344
|
+
entry_units = class_units(quantities, era)
|
|
1143
1345
|
units += entry_units
|
|
1144
1346
|
families[family] = families.get(family, 0.0) + entry_units
|
|
1145
1347
|
raw_total = sum(raw.values())
|
|
@@ -1186,7 +1388,8 @@ def in_progress_dates(segments, *, now) -> tuple:
|
|
|
1186
1388
|
if sl.kind == "in-progress"}))
|
|
1187
1389
|
|
|
1188
1390
|
|
|
1189
|
-
def unattributed_units(segments, entries, *, now
|
|
1391
|
+
def unattributed_units(segments, entries, *, now,
|
|
1392
|
+
era: CoefficientEra) -> dict:
|
|
1190
1393
|
"""Weighted units and entry count on NO observation's token interval.
|
|
1191
1394
|
|
|
1192
1395
|
Section 14 requires this diagnostic rather than a silent discard. An
|
|
@@ -1203,7 +1406,17 @@ def unattributed_units(segments, entries, *, now) -> dict:
|
|
|
1203
1406
|
the general-quota entries whose token split is known, because that is the
|
|
1204
1407
|
quantity the fit would have used; a dedicated-pool entry is reported as
|
|
1205
1408
|
present and contributes nothing, exactly as it would inside an interval.
|
|
1409
|
+
|
|
1410
|
+
`era` is the one coefficient era the series is weighted with, and the
|
|
1411
|
+
same out-of-era refusal applies as in `build_daily_series`.
|
|
1206
1412
|
"""
|
|
1413
|
+
_require_era(era)
|
|
1414
|
+
entries = list(entries)
|
|
1415
|
+
for entry in entries:
|
|
1416
|
+
require_aware(entry.at, "entry.at")
|
|
1417
|
+
_require_in_era(era, (e.at for e in entries), "entry.at")
|
|
1418
|
+
_require_in_era(era, (r.at for seg in segments for r in seg.rows),
|
|
1419
|
+
"snapshot.at")
|
|
1207
1420
|
windows = sorted((s.first_at, s.last_at)
|
|
1208
1421
|
for s in _day_slices(segments, now)
|
|
1209
1422
|
if s.kind == "attributed")
|
|
@@ -1218,7 +1431,7 @@ def unattributed_units(segments, entries, *, now) -> dict:
|
|
|
1218
1431
|
count += 1
|
|
1219
1432
|
if family_participation(normalize_family(entry.model)) != "general":
|
|
1220
1433
|
continue
|
|
1221
|
-
units = weighted_units(entry)
|
|
1434
|
+
units = weighted_units(entry, era)
|
|
1222
1435
|
if units is not None:
|
|
1223
1436
|
total += units
|
|
1224
1437
|
return {"units": total, "entries": count}
|
|
@@ -1317,7 +1530,7 @@ def support_radius(vectors, centre) -> float | None:
|
|
|
1317
1530
|
return median(distances) + MIX_SUPPORT["fence_mads"] * spread
|
|
1318
1531
|
|
|
1319
1532
|
|
|
1320
|
-
def token_class_floor(centre) -> float | None:
|
|
1533
|
+
def token_class_floor(centre, *, era: CoefficientEra) -> float | None:
|
|
1321
1534
|
"""Materiality floor for the token-class radius, at `centre`.
|
|
1322
1535
|
|
|
1323
1536
|
Derived, not asserted. For raw share distributions
|
|
@@ -1325,19 +1538,22 @@ def token_class_floor(centre) -> float | None:
|
|
|
1325
1538
|
movement of the weighted-unit total at
|
|
1326
1539
|
`MIX_SUPPORT["token_class_max_relative_effect"]` bounds the admissible
|
|
1327
1540
|
total-variation distance at `effect * mu / spread`, where `mu` is the
|
|
1328
|
-
centre's own weighted value and `spread` is the coefficient range
|
|
1329
|
-
|
|
1330
|
-
centre admits about 0.01058 — half the 10%
|
|
1331
|
-
third of the 15% prediction-width gate.
|
|
1541
|
+
centre's own weighted value and `spread` is the coefficient range of the
|
|
1542
|
+
selected `era` (#978). Under `opus-5` the spread is 4.73 - 0.0031 =
|
|
1543
|
+
4.7269, so a fresh-only centre admits about 0.01058 — half the 10%
|
|
1544
|
+
detection-width gate and a third of the 15% prediction-width gate. Under
|
|
1545
|
+
`opus-5-5` it is 1.0 - 0.0148 = 0.9852, and a fresh-only centre admits
|
|
1546
|
+
about 0.0507511.
|
|
1332
1547
|
|
|
1333
1548
|
None when the centre is undefined, which is a mix finding in its own right
|
|
1334
1549
|
and never reaches here.
|
|
1335
1550
|
"""
|
|
1551
|
+
_require_era(era)
|
|
1336
1552
|
if centre is None:
|
|
1337
1553
|
return None
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
spread = max(
|
|
1554
|
+
weights = era.weight_map()
|
|
1555
|
+
mu = class_units(centre, era)
|
|
1556
|
+
spread = max(weights.values()) - min(weights.values())
|
|
1341
1557
|
if spread <= 0.0 or not math.isfinite(mu) or mu <= 0.0:
|
|
1342
1558
|
return None
|
|
1343
1559
|
return float(MIX_SUPPORT["token_class_max_relative_effect"]) * mu / spread
|
|
@@ -1364,7 +1580,7 @@ def effective_radius(empirical, floor) -> float | None:
|
|
|
1364
1580
|
return max(empirical, floor)
|
|
1365
1581
|
|
|
1366
1582
|
|
|
1367
|
-
def aggregate_composition(entries) -> tuple | None:
|
|
1583
|
+
def aggregate_composition(entries, *, era: CoefficientEra) -> tuple | None:
|
|
1368
1584
|
"""One `(family_shares, class_shares)` pair over a set of entries.
|
|
1369
1585
|
|
|
1370
1586
|
The forecast population is a set of entries rather than a set of days
|
|
@@ -1374,8 +1590,16 @@ def aggregate_composition(entries) -> tuple | None:
|
|
|
1374
1590
|
|
|
1375
1591
|
None when no general-quota entry with a known token split contributes,
|
|
1376
1592
|
because an empty population supports no comparison.
|
|
1593
|
+
|
|
1594
|
+
`era` is the one coefficient era the population is weighted with; an
|
|
1595
|
+
entry outside it raises `CoefficientEraMismatch` (#978).
|
|
1377
1596
|
"""
|
|
1378
|
-
|
|
1597
|
+
_require_era(era)
|
|
1598
|
+
entries = list(entries)
|
|
1599
|
+
for entry in entries:
|
|
1600
|
+
require_aware(entry.at, "entry.at")
|
|
1601
|
+
_require_in_era(era, (e.at for e in entries), "entry.at")
|
|
1602
|
+
raw = {name: 0.0 for name in TOKEN_CLASSES}
|
|
1379
1603
|
families: dict = {}
|
|
1380
1604
|
for entry in entries:
|
|
1381
1605
|
family = normalize_family(entry.model)
|
|
@@ -1384,10 +1608,9 @@ def aggregate_composition(entries) -> tuple | None:
|
|
|
1384
1608
|
quantities = token_class_units(entry)
|
|
1385
1609
|
if quantities is None:
|
|
1386
1610
|
continue
|
|
1387
|
-
units = 0.0
|
|
1388
1611
|
for name, qty in quantities.items():
|
|
1389
1612
|
raw[name] += qty
|
|
1390
|
-
|
|
1613
|
+
units = class_units(quantities, era)
|
|
1391
1614
|
families[family] = families.get(family, 0.0) + units
|
|
1392
1615
|
raw_total = sum(raw.values())
|
|
1393
1616
|
unit_total = sum(families.values())
|
|
@@ -2160,6 +2383,7 @@ def _composition_provenance(*, confirmed, reference_days, decisive_days,
|
|
|
2160
2383
|
|
|
2161
2384
|
|
|
2162
2385
|
def analyse(series, *, now, newest_at, forecast_population,
|
|
2386
|
+
era: CoefficientEra, current_week_withheld: "str | None" = None,
|
|
2163
2387
|
fingerprint_matches=True, observed_percent=None,
|
|
2164
2388
|
current_week_units=None, current_week_start=None,
|
|
2165
2389
|
current_week_end=None, override_split=None, extra_statuses=(),
|
|
@@ -2199,7 +2423,16 @@ def analyse(series, *, now, newest_at, forecast_population,
|
|
|
2199
2423
|
non-finite or negative value withholds the evidence that depends on it
|
|
2200
2424
|
with a typed code, because a NaN propagates through the headroom
|
|
2201
2425
|
arithmetic into a published interval instead of raising.
|
|
2426
|
+
|
|
2427
|
+
`era` is the ONE coefficient era the analysis is fitted in (#978): the
|
|
2428
|
+
support floor uses its weights and the forecast probe aggregates under
|
|
2429
|
+
it, and a forecast entry outside it raises `CoefficientEraMismatch`.
|
|
2430
|
+
`current_week_withheld`, when set, is an `EVIDENCE_CODES` member that
|
|
2431
|
+
withholds the current week's consumption, headroom and projection while
|
|
2432
|
+
leaving `fitted` untouched — the glue's rule for a subscription week that
|
|
2433
|
+
spans the era boundary, whose units must never be summed across eras.
|
|
2202
2434
|
"""
|
|
2435
|
+
_require_era(era)
|
|
2203
2436
|
require_aware(now, "now")
|
|
2204
2437
|
for label, value in (("newest_at", newest_at),
|
|
2205
2438
|
("current_week_start", current_week_start),
|
|
@@ -2209,6 +2442,8 @@ def analyse(series, *, now, newest_at, forecast_population,
|
|
|
2209
2442
|
forecast_entries = list(forecast_population)
|
|
2210
2443
|
for entry in forecast_entries:
|
|
2211
2444
|
require_aware(entry.at, "forecast_entry.at")
|
|
2445
|
+
_require_in_era(era, (e.at for e in forecast_entries),
|
|
2446
|
+
"forecast_entry.at")
|
|
2212
2447
|
|
|
2213
2448
|
rejected: list[str] = []
|
|
2214
2449
|
if current_week_units is not None and not _usable_quantity(
|
|
@@ -2278,7 +2513,7 @@ def analyse(series, *, now, newest_at, forecast_population,
|
|
|
2278
2513
|
family_radius = effective_radius(
|
|
2279
2514
|
family_radius, float(MIX_SUPPORT["family_tv_floor"]))
|
|
2280
2515
|
class_radius = effective_radius(
|
|
2281
|
-
class_radius, token_class_floor(class_centre))
|
|
2516
|
+
class_radius, token_class_floor(class_centre, era=era))
|
|
2282
2517
|
|
|
2283
2518
|
# Section 35: after a confirmed change only the entries at or after the
|
|
2284
2519
|
# successor boundary belong to the current regime, and they are measured
|
|
@@ -2289,7 +2524,7 @@ def analyse(series, *, now, newest_at, forecast_population,
|
|
|
2289
2524
|
if e.at.astimezone(dt.timezone.utc).date() >= detector.split_date]
|
|
2290
2525
|
else:
|
|
2291
2526
|
probed_entries = forecast_entries
|
|
2292
|
-
forecast_shares = (aggregate_composition(probed_entries)
|
|
2527
|
+
forecast_shares = (aggregate_composition(probed_entries, era=era)
|
|
2293
2528
|
if probed_entries else None)
|
|
2294
2529
|
|
|
2295
2530
|
contributed = list(extra_statuses)
|
|
@@ -2431,6 +2666,13 @@ def analyse(series, *, now, newest_at, forecast_population,
|
|
|
2431
2666
|
week_start=current_week_start,
|
|
2432
2667
|
week_end=current_week_end,
|
|
2433
2668
|
qualifications=published_qualifications)
|
|
2669
|
+
if current_week_withheld is not None:
|
|
2670
|
+
consumption = evidence_withheld(current_week_withheld, population,
|
|
2671
|
+
published_qualifications)
|
|
2672
|
+
headroom = evidence_withheld(current_week_withheld, population,
|
|
2673
|
+
published_qualifications)
|
|
2674
|
+
projection = evidence_withheld(current_week_withheld, population,
|
|
2675
|
+
published_qualifications)
|
|
2434
2676
|
|
|
2435
2677
|
merged = dict(diagnostics or {})
|
|
2436
2678
|
merged.update({
|