cctally 1.115.0 → 1.116.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/bin/_cctally_cache.py +85 -4
- package/bin/_cctally_completion.py +80 -12
- package/bin/_cctally_core.py +106 -9
- package/bin/_cctally_dashboard.py +6 -9
- package/bin/_cctally_dashboard_envelope.py +26 -32
- package/bin/_cctally_db.py +85 -4
- package/bin/_cctally_doctor.py +38 -5
- package/bin/_cctally_five_hour.py +83 -1017
- package/bin/_cctally_journal.py +1307 -180
- package/bin/_cctally_journal_repair.py +9 -7
- package/bin/_cctally_oauth_samples.py +280 -0
- package/bin/_cctally_parser.py +5 -0
- package/bin/_cctally_quarantine.py +14 -4
- package/bin/_cctally_quota.py +70 -8
- package/bin/_cctally_quota_model.py +35 -4
- package/bin/_cctally_record.py +414 -123
- package/bin/_cctally_rederive.py +632 -235
- package/bin/_cctally_refresh.py +29 -0
- package/bin/_cctally_statusline.py +984 -47
- package/bin/_cctally_store.py +972 -58
- package/bin/_cctally_tui.py +62 -7
- package/bin/_cctally_update.py +25 -0
- package/bin/_lib_account_protocol.py +201 -18
- package/bin/_lib_clone_copy.py +46 -11
- package/bin/_lib_dashboard_sources.py +57 -15
- package/bin/_lib_detector_access_registry.py +273 -52
- package/bin/_lib_doctor.py +302 -1
- package/bin/_lib_journal.py +145 -25
- package/bin/_lib_journal_router.py +166 -48
- package/bin/_lib_oauth_samples.py +238 -0
- package/bin/_lib_pricing_check.py +0 -12
- package/bin/_lib_provider_activity.py +770 -0
- package/bin/_lib_quarantine.py +3 -2
- package/bin/_lib_quota_model.py +96 -6
- package/bin/_lib_rebuild_progress.py +146 -0
- package/bin/_lib_record.py +22 -0
- package/bin/_lib_rederive.py +299 -3
- package/bin/_lib_snapshot_cache.py +43 -1
- package/bin/_lib_stale_proof.py +708 -0
- package/bin/_lib_statusline_candidates.py +65 -5
- package/bin/_lib_worker_launches.py +315 -0
- package/bin/cctally +6 -11
- 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 +7 -2
- package/bin/_lib_merge_gate.py +0 -452
- package/dashboard/static/assets/DoctorModal-D--qVBwe.js +0 -1
package/bin/_lib_quarantine.py
CHANGED
|
@@ -9,8 +9,9 @@ Stdlib only. An entry's `id` takes one of two forms:
|
|
|
9
9
|
the `NAME` of `bin/cctally-NAME-test` and `<case>` is one scenario of that
|
|
10
10
|
harness's own case inventory (`bin/cctally-NAME-test --list-cases`).
|
|
11
11
|
|
|
12
|
-
`bin/cctally-test-all
|
|
13
|
-
list through this module
|
|
12
|
+
`bin/cctally-test-all` (through `bin/_lib_test_selection.py`) and the quick
|
|
13
|
+
gate read the list through this module, and so does the nightly broad run's
|
|
14
|
+
quarantine leg (bin/cctally-suite-run). An
|
|
14
15
|
absent list quarantines nothing (the public mirror may carry none); a list
|
|
15
16
|
that exists but cannot be read raises QuarantineError, never "nothing listed".
|
|
16
17
|
|
package/bin/_lib_quota_model.py
CHANGED
|
@@ -21,6 +21,7 @@ from __future__ import annotations
|
|
|
21
21
|
import bisect
|
|
22
22
|
import dataclasses
|
|
23
23
|
import datetime as dt
|
|
24
|
+
import decimal
|
|
24
25
|
import enum
|
|
25
26
|
import hashlib
|
|
26
27
|
import json
|
|
@@ -1437,6 +1438,84 @@ def unattributed_units(segments, entries, *, now,
|
|
|
1437
1438
|
return {"units": total, "entries": count}
|
|
1438
1439
|
|
|
1439
1440
|
|
|
1441
|
+
# ---------------------------------------------------------------------------
|
|
1442
|
+
# Correctly rounded logarithms and exponentials.
|
|
1443
|
+
#
|
|
1444
|
+
# `math.log`, `math.log1p` and `math.expm1` call the platform C library, and
|
|
1445
|
+
# C does not require those to be correctly rounded. glibc 2.39's `log1p` and
|
|
1446
|
+
# `expm1` are off by one ULP on inputs the eligibility fence reaches, which
|
|
1447
|
+
# moved quota fixture 12's `eligibilityFence` between Linux and macOS. These
|
|
1448
|
+
# compute in `decimal`, whose `ln` and `exp` are correctly rounded at the
|
|
1449
|
+
# context precision, from the float's exact value, and round once to a
|
|
1450
|
+
# double. At 80 significant digits that double rounding can differ from a
|
|
1451
|
+
# direct one only if the true value lies within 1e-80 of a midpoint between
|
|
1452
|
+
# two doubles, far below what any double input can produce. Each call uses
|
|
1453
|
+
# its own context, never the thread's, and the edge cases the decimal path
|
|
1454
|
+
# does not model are delegated to `math`, whose answers there are exact.
|
|
1455
|
+
# ---------------------------------------------------------------------------
|
|
1456
|
+
_CR_DIGITS = 80
|
|
1457
|
+
#: The exact sum of 1 and any double needs at most 1075 significant digits.
|
|
1458
|
+
_CR_EXACT = decimal.Context(prec=1200, Emin=-999999, Emax=999999,
|
|
1459
|
+
traps=[decimal.InvalidOperation])
|
|
1460
|
+
_CR_ONE = decimal.Decimal(1)
|
|
1461
|
+
|
|
1462
|
+
|
|
1463
|
+
def _cr_context(prec: int = _CR_DIGITS) -> decimal.Context:
|
|
1464
|
+
return decimal.Context(prec=prec, rounding=decimal.ROUND_HALF_EVEN,
|
|
1465
|
+
Emin=-999999, Emax=999999,
|
|
1466
|
+
traps=[decimal.InvalidOperation,
|
|
1467
|
+
decimal.DivisionByZero, decimal.Overflow])
|
|
1468
|
+
|
|
1469
|
+
|
|
1470
|
+
def correctly_rounded_log(x) -> float:
|
|
1471
|
+
"""`math.log(x)` correctly rounded on every platform.
|
|
1472
|
+
|
|
1473
|
+
Zero, negatives, infinities and NaN go to `math.log`, so the domain error
|
|
1474
|
+
and the non-finite results are exactly the standard library's.
|
|
1475
|
+
"""
|
|
1476
|
+
x = float(x)
|
|
1477
|
+
if not math.isfinite(x) or x <= 0.0:
|
|
1478
|
+
return math.log(x)
|
|
1479
|
+
return float(_cr_context().ln(decimal.Decimal(x)))
|
|
1480
|
+
|
|
1481
|
+
|
|
1482
|
+
def correctly_rounded_log1p(x) -> float:
|
|
1483
|
+
"""`math.log1p(x)` correctly rounded on every platform.
|
|
1484
|
+
|
|
1485
|
+
`1 + x` is formed exactly, so a tiny `x` keeps every digit. Signed zeros,
|
|
1486
|
+
`x <= -1` (the domain error), infinities and NaN go to `math.log1p`.
|
|
1487
|
+
"""
|
|
1488
|
+
x = float(x)
|
|
1489
|
+
if not math.isfinite(x) or x == 0.0 or x <= -1.0:
|
|
1490
|
+
return math.log1p(x)
|
|
1491
|
+
one_plus = _CR_EXACT.add(_CR_ONE, decimal.Decimal(x))
|
|
1492
|
+
return float(_cr_context().ln(one_plus))
|
|
1493
|
+
|
|
1494
|
+
|
|
1495
|
+
def correctly_rounded_expm1(x) -> float:
|
|
1496
|
+
"""`math.expm1(x)` correctly rounded on every platform.
|
|
1497
|
+
|
|
1498
|
+
The working precision grows with `1/|x|`, so the subtraction of one keeps
|
|
1499
|
+
80 significant digits however small `x` is. Below -64 the true value is
|
|
1500
|
+
within 1.6e-28 of -1, which rounds to exactly -1. A finite result too
|
|
1501
|
+
large for a double raises `OverflowError`, as `math.expm1` does. Signed
|
|
1502
|
+
zeros, infinities and NaN go to `math.expm1`.
|
|
1503
|
+
"""
|
|
1504
|
+
x = float(x)
|
|
1505
|
+
if not math.isfinite(x) or x == 0.0:
|
|
1506
|
+
return math.expm1(x)
|
|
1507
|
+
if x < -64.0:
|
|
1508
|
+
return -1.0
|
|
1509
|
+
if x > 710.0:
|
|
1510
|
+
return math.expm1(x) # raises OverflowError
|
|
1511
|
+
d = decimal.Decimal(x)
|
|
1512
|
+
ctx = _cr_context(_CR_DIGITS + max(0, -d.adjusted()))
|
|
1513
|
+
result = float(ctx.subtract(ctx.exp(d), _CR_ONE))
|
|
1514
|
+
if math.isinf(result):
|
|
1515
|
+
raise OverflowError("math range error")
|
|
1516
|
+
return result
|
|
1517
|
+
|
|
1518
|
+
|
|
1440
1519
|
# ---------------------------------------------------------------------------
|
|
1441
1520
|
# Robust statistics, the eligibility fence, and composition support (§5, §15).
|
|
1442
1521
|
# ---------------------------------------------------------------------------
|
|
@@ -1476,14 +1555,16 @@ def eligibility_fence(days) -> float | None:
|
|
|
1476
1555
|
which carried between 0.8 and 1.2 million weighted units against a
|
|
1477
1556
|
message claiming no local token history at all.
|
|
1478
1557
|
"""
|
|
1479
|
-
xs = [
|
|
1558
|
+
xs = [correctly_rounded_log1p(u)
|
|
1559
|
+
for u in days if u is not None and u > 0.0]
|
|
1480
1560
|
if len(xs) < 2:
|
|
1481
1561
|
return None
|
|
1482
1562
|
centre = median(xs)
|
|
1483
1563
|
spread = scaled_mad(xs, centre)
|
|
1484
1564
|
if not math.isfinite(spread) or spread == 0.0:
|
|
1485
1565
|
return None
|
|
1486
|
-
return
|
|
1566
|
+
return correctly_rounded_expm1(
|
|
1567
|
+
centre - ELIGIBILITY["fence_mads"] * spread)
|
|
1487
1568
|
|
|
1488
1569
|
|
|
1489
1570
|
def composition_centre(vectors) -> dict[str, float] | None:
|
|
@@ -1506,9 +1587,16 @@ def composition_centre(vectors) -> dict[str, float] | None:
|
|
|
1506
1587
|
|
|
1507
1588
|
|
|
1508
1589
|
def tv_distance(p, centre) -> float:
|
|
1509
|
-
"""Total-variation distance, `0.5 * sum(|p_i - c_i|)`.
|
|
1590
|
+
"""Total-variation distance, `0.5 * sum(|p_i - c_i|)`.
|
|
1591
|
+
|
|
1592
|
+
`math.fsum`, not `sum`: the keys come from a set, whose iteration order
|
|
1593
|
+
follows PYTHONHASHSEED, and Python 3.11's float `sum()` is uncompensated,
|
|
1594
|
+
so a plain sum moved the published radius by an ULP from one process to
|
|
1595
|
+
the next. `fsum` is correctly rounded, so the result is the same in every
|
|
1596
|
+
order and on every supported interpreter.
|
|
1597
|
+
"""
|
|
1510
1598
|
keys = set(p) | set(centre)
|
|
1511
|
-
return 0.5 *
|
|
1599
|
+
return 0.5 * math.fsum(abs(p.get(k, 0.0) - centre.get(k, 0.0)) for k in keys)
|
|
1512
1600
|
|
|
1513
1601
|
|
|
1514
1602
|
def support_radius(vectors, centre) -> float | None:
|
|
@@ -1696,7 +1784,9 @@ def jackknife_fit(days) -> tuple[float, Interval] | None:
|
|
|
1696
1784
|
return None
|
|
1697
1785
|
replicates.append((total_units - row.units) / rest_meter)
|
|
1698
1786
|
mean = sum(replicates) / n
|
|
1699
|
-
|
|
1787
|
+
# A product, not `** 2`, which is the C library's `pow`.
|
|
1788
|
+
se = math.sqrt((n - 1) / n * sum((v - mean) * (v - mean)
|
|
1789
|
+
for v in replicates))
|
|
1700
1790
|
half = t_quantile_975(n - 1) * se
|
|
1701
1791
|
return point, Interval(max(0.0, point - half), point + half)
|
|
1702
1792
|
|
|
@@ -1930,7 +2020,7 @@ def detect_change(series, *, override_split=None) -> DetectorResult:
|
|
|
1930
2020
|
total = len(days)
|
|
1931
2021
|
truncated = input_days - total
|
|
1932
2022
|
scan_start = days[0].date if days else None
|
|
1933
|
-
stats = [
|
|
2023
|
+
stats = [correctly_rounded_log(o.units / o.meter_delta) for o in days]
|
|
1934
2024
|
min_base = int(TRUST["min_detect_baseline_days"])
|
|
1935
2025
|
min_watch = int(DETECTOR["min_watch_days"])
|
|
1936
2026
|
fence_mads = DETECTOR["fence_mads"]
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""Progress and resident memory for the stats index's full replays (#997).
|
|
2
|
+
|
|
3
|
+
The 2026-10-22 epoch rebuild ran 4.4 hours and died without a word, because
|
|
4
|
+
nothing it did between start and death reached a file. This module is the seam
|
|
5
|
+
a detached worker's state file hangs off (spec D9):
|
|
6
|
+
|
|
7
|
+
* ``install(sink)`` / ``uninstall()``: one process-wide sink, called as
|
|
8
|
+
``sink(phase, counters, forced)``. Only a worker holding its flock installs
|
|
9
|
+
one; everywhere else every call below is a no-op.
|
|
10
|
+
* ``phase(name, **counters)``: where the replay is now. Always written
|
|
11
|
+
(``forced``); the counters start over with the ones given.
|
|
12
|
+
* ``tick(**counters)``: merges counters into the current phase's and writes
|
|
13
|
+
them at most every ``THROTTLE_SECONDS`` (``time.monotonic``), so a call
|
|
14
|
+
site can tick on every batch without turning progress into I/O.
|
|
15
|
+
* ``waiting(lock)``: a context for a blocking lock wait, written as the phase
|
|
16
|
+
``waiting:<lock>`` and replaced by the interrupted phase when it ends.
|
|
17
|
+
|
|
18
|
+
A sink that raises is ignored: progress never fails a rebuild.
|
|
19
|
+
|
|
20
|
+
``peak_resident_bytes`` is the process-lifetime peak the rebuild record and the
|
|
21
|
+
worker state report as ``peakResidentBytes`` (spec D11). ``peakHeapBytes``, a
|
|
22
|
+
``tracemalloc`` peak that is zero unless tracing, never saw the memory that
|
|
23
|
+
pushed that rebuild into hours of compression.
|
|
24
|
+
|
|
25
|
+
Stdlib only, and importable on its own.
|
|
26
|
+
"""
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import contextlib
|
|
30
|
+
import os
|
|
31
|
+
import resource
|
|
32
|
+
import subprocess
|
|
33
|
+
import sys
|
|
34
|
+
import time
|
|
35
|
+
|
|
36
|
+
#: The longest a ticking replay goes without a write (spec D9).
|
|
37
|
+
THROTTLE_SECONDS = 10.0
|
|
38
|
+
|
|
39
|
+
_sink = None
|
|
40
|
+
_phase: "str | None" = None
|
|
41
|
+
_counters: dict = {}
|
|
42
|
+
_last_write: "float | None" = None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def install(sink) -> None:
|
|
46
|
+
"""Route progress to ``sink(phase, counters, forced)`` from now on."""
|
|
47
|
+
global _sink, _phase, _counters, _last_write
|
|
48
|
+
_sink = sink
|
|
49
|
+
_phase = None
|
|
50
|
+
_counters = {}
|
|
51
|
+
_last_write = None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def uninstall() -> None:
|
|
55
|
+
"""Stop routing progress; every call becomes a no-op again."""
|
|
56
|
+
global _sink, _phase, _counters, _last_write
|
|
57
|
+
_sink = None
|
|
58
|
+
_phase = None
|
|
59
|
+
_counters = {}
|
|
60
|
+
_last_write = None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _emit(forced: bool) -> None:
|
|
64
|
+
global _last_write
|
|
65
|
+
_last_write = time.monotonic()
|
|
66
|
+
sink = _sink
|
|
67
|
+
if sink is None:
|
|
68
|
+
return
|
|
69
|
+
try:
|
|
70
|
+
sink(_phase, dict(_counters), forced)
|
|
71
|
+
except Exception: # progress never fails a rebuild
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def phase(name: str, **counters) -> None:
|
|
76
|
+
"""Enter phase ``name`` and write it now."""
|
|
77
|
+
global _phase, _counters
|
|
78
|
+
if _sink is None:
|
|
79
|
+
return
|
|
80
|
+
_phase = name
|
|
81
|
+
_counters = dict(counters)
|
|
82
|
+
_emit(True)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def tick(**counters) -> None:
|
|
86
|
+
"""Record ``counters`` and write them if the throttle allows."""
|
|
87
|
+
if _sink is None:
|
|
88
|
+
return
|
|
89
|
+
_counters.update(counters)
|
|
90
|
+
if (_last_write is not None
|
|
91
|
+
and time.monotonic() - _last_write < THROTTLE_SECONDS):
|
|
92
|
+
return
|
|
93
|
+
_emit(False)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@contextlib.contextmanager
|
|
97
|
+
def waiting(lock: str):
|
|
98
|
+
"""Report a blocking wait on ``lock`` for as long as the body runs."""
|
|
99
|
+
global _phase, _counters
|
|
100
|
+
if _sink is None:
|
|
101
|
+
yield
|
|
102
|
+
return
|
|
103
|
+
previous, saved = _phase, dict(_counters)
|
|
104
|
+
phase(f"waiting:{lock}")
|
|
105
|
+
try:
|
|
106
|
+
yield
|
|
107
|
+
finally:
|
|
108
|
+
if _sink is not None:
|
|
109
|
+
_phase, _counters = previous, saved
|
|
110
|
+
_emit(True)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def peak_resident_bytes() -> int:
|
|
114
|
+
"""``getrusage(RUSAGE_SELF).ru_maxrss`` in bytes.
|
|
115
|
+
|
|
116
|
+
macOS reports bytes and Linux reports KiB; the unit is decided per call
|
|
117
|
+
from ``sys.platform`` so a test can exercise both branches.
|
|
118
|
+
"""
|
|
119
|
+
peak = int(resource.getrusage(resource.RUSAGE_SELF).ru_maxrss)
|
|
120
|
+
if sys.platform == "darwin":
|
|
121
|
+
return peak
|
|
122
|
+
return peak * 1024
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def current_resident_bytes() -> "int | None":
|
|
126
|
+
"""This process's current resident set in bytes, or None when unknown.
|
|
127
|
+
|
|
128
|
+
Best effort, because no stdlib call reports it on macOS: Linux reads
|
|
129
|
+
``/proc/self/statm``; elsewhere ``ps -o rss=`` (KiB) is asked under
|
|
130
|
+
``LC_ALL=C``. Any failure answers None rather than raising, since a
|
|
131
|
+
progress reading must never fail a rebuild.
|
|
132
|
+
"""
|
|
133
|
+
try:
|
|
134
|
+
with open("/proc/self/statm", "rb") as handle:
|
|
135
|
+
fields = handle.read().split()
|
|
136
|
+
return int(fields[1]) * os.sysconf("SC_PAGE_SIZE")
|
|
137
|
+
except (OSError, ValueError, IndexError):
|
|
138
|
+
pass
|
|
139
|
+
try:
|
|
140
|
+
out = subprocess.run(
|
|
141
|
+
["ps", "-o", "rss=", "-p", str(os.getpid())],
|
|
142
|
+
capture_output=True, text=True, timeout=5,
|
|
143
|
+
env={**os.environ, "LC_ALL": "C"}, check=False)
|
|
144
|
+
return int(out.stdout.strip()) * 1024
|
|
145
|
+
except (OSError, ValueError, subprocess.SubprocessError):
|
|
146
|
+
return None
|
package/bin/_lib_record.py
CHANGED
|
@@ -150,6 +150,7 @@ class FiveHourSourceDecision:
|
|
|
150
150
|
def plan_five_hour_source_local_credit(
|
|
151
151
|
*, baseline_pct, pending_low_pct, pending_observation_id,
|
|
152
152
|
new_pct, observation_id, drop_threshold, window_live,
|
|
153
|
+
generation=None, baseline_generation=None, pending_generation=None,
|
|
153
154
|
):
|
|
154
155
|
"""Classify one observation against ONE contributor's own five-hour state
|
|
155
156
|
(glue call site: ``detect_reset_and_credit``'s 5h branch).
|
|
@@ -173,9 +174,30 @@ def plan_five_hour_source_local_credit(
|
|
|
173
174
|
is deliberately no maximum gap, no maximum climb and no confirmation-value
|
|
174
175
|
band, because observations arrive at an unbounded interval and every finite
|
|
175
176
|
threshold on the change between two of them has a legitimate crossing.
|
|
177
|
+
|
|
178
|
+
``generation`` (#797/#823/#840 spec 4.9, D3/D11) is the account and
|
|
179
|
+
window's current five-hour credit generation, passed only by the v2
|
|
180
|
+
detector; the historical detector passes None and keeps every rule above
|
|
181
|
+
unchanged. ``baseline_generation`` and ``pending_generation`` are the
|
|
182
|
+
generations the source's baseline and its pending descent were
|
|
183
|
+
established under. A state established under another generation is
|
|
184
|
+
OBSOLETE: a credit has been recorded, corrected or tombstoned since, so
|
|
185
|
+
its baseline may be a pre-credit high and its descent may be one another
|
|
186
|
+
source already confirmed. It can neither arm nor confirm, and it is not
|
|
187
|
+
relabelled into the new generation; this observation re-establishes the
|
|
188
|
+
baseline instead. A state recorded without a generation (the historical
|
|
189
|
+
detector, an earlier binary, or a seeded fixture) is not obsolete by
|
|
190
|
+
this rule.
|
|
176
191
|
"""
|
|
177
192
|
new_pct = float(new_pct)
|
|
178
193
|
|
|
194
|
+
if baseline_pct is not None and generation is not None and (
|
|
195
|
+
(baseline_generation is not None
|
|
196
|
+
and baseline_generation != generation)
|
|
197
|
+
or (pending_low_pct is not None and pending_generation is not None
|
|
198
|
+
and pending_generation != generation)):
|
|
199
|
+
return FiveHourSourceDecision(FIVE_HOUR_SET_BASELINE, new_pct)
|
|
200
|
+
|
|
179
201
|
if baseline_pct is None:
|
|
180
202
|
# No state for this source in this window. Establish the baseline and
|
|
181
203
|
# emit nothing: a first reading is not a descent, whatever any other
|