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/_cctally_record.py
CHANGED
|
@@ -3428,8 +3428,13 @@ def _read_five_hour_source_state(conn, account_key, five_hour_window_key,
|
|
|
3428
3428
|
"""Return one contributor's five-hour state, or None when it has none.
|
|
3429
3429
|
|
|
3430
3430
|
The tuple is ``(baseline_pct, pending_low_pct, pending_at_utc,
|
|
3431
|
-
pending_observation_id
|
|
3432
|
-
|
|
3431
|
+
pending_observation_id, baseline_observation_id, baseline_generation,
|
|
3432
|
+
pending_generation, pending_position)``; ``pending_low_pct is not None``
|
|
3433
|
+
is the armed predicate. The last four are the v2 detector's causal facts
|
|
3434
|
+
(#797/#823/#840 spec 4.9): the observation that established the baseline,
|
|
3435
|
+
the credit generation the baseline and the pending descent were
|
|
3436
|
+
established under, and the arming observation's capture instant. Each is
|
|
3437
|
+
None on a row the historical detector or an earlier binary wrote. A missing table is reported as "no state" rather than raised,
|
|
3433
3438
|
for the same reason the weekly debounce reader does it: the detector must
|
|
3434
3439
|
never crash a recording tick over its own bookkeeping.
|
|
3435
3440
|
|
|
@@ -3467,7 +3472,9 @@ def _read_five_hour_source_state(conn, account_key, five_hour_window_key,
|
|
|
3467
3472
|
try:
|
|
3468
3473
|
row = conn.execute(
|
|
3469
3474
|
"SELECT baseline_pct, pending_low_pct, pending_at_utc, "
|
|
3470
|
-
" pending_observation_id "
|
|
3475
|
+
" pending_observation_id, baseline_observation_id, "
|
|
3476
|
+
" baseline_generation, pending_generation, "
|
|
3477
|
+
" pending_position "
|
|
3471
3478
|
" FROM five_hour_credit_confirmation_state "
|
|
3472
3479
|
" WHERE account_key = ? AND five_hour_window_key = ? "
|
|
3473
3480
|
" AND source = ?",
|
|
@@ -3482,19 +3489,31 @@ def _read_five_hour_source_state(conn, account_key, five_hour_window_key,
|
|
|
3482
3489
|
None if row[1] is None else float(row[1]),
|
|
3483
3490
|
row[2],
|
|
3484
3491
|
row[3],
|
|
3492
|
+
row[4],
|
|
3493
|
+
row[5],
|
|
3494
|
+
row[6],
|
|
3495
|
+
row[7],
|
|
3485
3496
|
)
|
|
3486
3497
|
|
|
3487
3498
|
|
|
3488
3499
|
def _write_five_hour_source_state(conn, account_key, five_hour_window_key,
|
|
3489
3500
|
source, *, baseline_pct,
|
|
3490
3501
|
pending_low_pct=None, pending_at_utc=None,
|
|
3491
|
-
pending_observation_id=None
|
|
3502
|
+
pending_observation_id=None,
|
|
3503
|
+
baseline_observation_id=None,
|
|
3504
|
+
baseline_generation=None,
|
|
3505
|
+
pending_generation=None,
|
|
3506
|
+
pending_position=None):
|
|
3492
3507
|
"""Upsert one contributor's state for one window, then retire that
|
|
3493
3508
|
contributor's rows for earlier windows.
|
|
3494
3509
|
|
|
3495
3510
|
The three ``pending_*`` values are written together on every call, so
|
|
3496
3511
|
cancelling or confirming a descent is the same statement as arming one and
|
|
3497
|
-
a stale pending value can never survive a state change.
|
|
3512
|
+
a stale pending value can never survive a state change. The same holds
|
|
3513
|
+
for the v2 causal facts (``baseline_observation_id``,
|
|
3514
|
+
``baseline_generation``, ``pending_generation``, ``pending_position``):
|
|
3515
|
+
every write states all of them, so the historical detector's write
|
|
3516
|
+
leaves none behind.
|
|
3498
3517
|
|
|
3499
3518
|
``pending_at_utc`` is the arming tick's DETECTION clock, which is what the
|
|
3500
3519
|
five-hour path has always stamped into `effective_reset_at_utc`
|
|
@@ -3512,16 +3531,23 @@ def _write_five_hour_source_state(conn, account_key, five_hour_window_key,
|
|
|
3512
3531
|
conn.execute(
|
|
3513
3532
|
"INSERT INTO five_hour_credit_confirmation_state "
|
|
3514
3533
|
"(account_key, five_hour_window_key, source, baseline_pct, "
|
|
3515
|
-
" pending_low_pct, pending_at_utc, pending_observation_id
|
|
3516
|
-
"
|
|
3534
|
+
" pending_low_pct, pending_at_utc, pending_observation_id, "
|
|
3535
|
+
" baseline_observation_id, baseline_generation, pending_generation, "
|
|
3536
|
+
" pending_position) "
|
|
3537
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) "
|
|
3517
3538
|
"ON CONFLICT(account_key, five_hour_window_key, source) DO UPDATE SET "
|
|
3518
3539
|
" baseline_pct = excluded.baseline_pct, "
|
|
3519
3540
|
" pending_low_pct = excluded.pending_low_pct, "
|
|
3520
3541
|
" pending_at_utc = excluded.pending_at_utc, "
|
|
3521
|
-
" pending_observation_id = excluded.pending_observation_id"
|
|
3542
|
+
" pending_observation_id = excluded.pending_observation_id, "
|
|
3543
|
+
" baseline_observation_id = excluded.baseline_observation_id, "
|
|
3544
|
+
" baseline_generation = excluded.baseline_generation, "
|
|
3545
|
+
" pending_generation = excluded.pending_generation, "
|
|
3546
|
+
" pending_position = excluded.pending_position",
|
|
3522
3547
|
(account_key, int(five_hour_window_key), source, float(baseline_pct),
|
|
3523
3548
|
None if pending_low_pct is None else float(pending_low_pct),
|
|
3524
|
-
pending_at_utc, pending_observation_id
|
|
3549
|
+
pending_at_utc, pending_observation_id, baseline_observation_id,
|
|
3550
|
+
baseline_generation, pending_generation, pending_position),
|
|
3525
3551
|
)
|
|
3526
3552
|
conn.execute(
|
|
3527
3553
|
"DELETE FROM five_hour_credit_confirmation_state "
|
|
@@ -3532,7 +3558,8 @@ def _write_five_hour_source_state(conn, account_key, five_hour_window_key,
|
|
|
3532
3558
|
|
|
3533
3559
|
def _step_five_hour_source_state(conn, account_key, five_hour_window_key,
|
|
3534
3560
|
source, *, new_pct, observation_id,
|
|
3535
|
-
drop_threshold, window_live, now_iso
|
|
3561
|
+
drop_threshold, window_live, now_iso,
|
|
3562
|
+
generation=None, capture_at=None):
|
|
3536
3563
|
"""Advance one contributor's five-hour state by one observation.
|
|
3537
3564
|
|
|
3538
3565
|
Returns ``(decision, prior_state)``, where ``prior_state`` is the row as it
|
|
@@ -3545,6 +3572,14 @@ def _step_five_hour_source_state(conn, account_key, five_hour_window_key,
|
|
|
3545
3572
|
must leave the state armed so the next same-source reading below the
|
|
3546
3573
|
baseline re-confirms and re-runs the idempotent pivots, rather than losing
|
|
3547
3574
|
the credit outright.
|
|
3575
|
+
|
|
3576
|
+
``generation`` (#797/#823/#840 spec 4.9) is the account and window's
|
|
3577
|
+
current five-hour credit generation, passed only by the v2 detector. It
|
|
3578
|
+
makes a state established under another generation obsolete (the
|
|
3579
|
+
kernel), and it is recorded with what this observation establishes: the
|
|
3580
|
+
baseline's observation and generation, and an arming observation's
|
|
3581
|
+
generation and causal position (its capture instant, ``capture_at``). The
|
|
3582
|
+
historical detector passes None and records none of these.
|
|
3548
3583
|
"""
|
|
3549
3584
|
prior_state = _read_five_hour_source_state(
|
|
3550
3585
|
conn, account_key, five_hour_window_key, source)
|
|
@@ -3556,7 +3591,11 @@ def _step_five_hour_source_state(conn, account_key, five_hour_window_key,
|
|
|
3556
3591
|
observation_id=observation_id,
|
|
3557
3592
|
drop_threshold=drop_threshold,
|
|
3558
3593
|
window_live=window_live,
|
|
3594
|
+
generation=generation,
|
|
3595
|
+
baseline_generation=None if prior_state is None else prior_state[5],
|
|
3596
|
+
pending_generation=None if prior_state is None else prior_state[6],
|
|
3559
3597
|
)
|
|
3598
|
+
v2 = generation is not None
|
|
3560
3599
|
if decision.action == FIVE_HOUR_ARM:
|
|
3561
3600
|
# The descent is recorded and nothing is emitted. The write clamp
|
|
3562
3601
|
# raises this reading back to the baseline in
|
|
@@ -3569,13 +3608,26 @@ def _step_five_hour_source_state(conn, account_key, five_hour_window_key,
|
|
|
3569
3608
|
pending_low_pct=new_pct,
|
|
3570
3609
|
pending_at_utc=now_iso,
|
|
3571
3610
|
pending_observation_id=observation_id,
|
|
3611
|
+
baseline_observation_id=(
|
|
3612
|
+
prior_state[4] if v2 and prior_state is not None else None),
|
|
3613
|
+
baseline_generation=generation,
|
|
3614
|
+
pending_generation=generation,
|
|
3615
|
+
pending_position=capture_at if v2 else None,
|
|
3572
3616
|
)
|
|
3573
3617
|
elif decision.action in (FIVE_HOUR_SET_BASELINE, FIVE_HOUR_CANCEL):
|
|
3574
3618
|
# Establish, raise, or cancel. All three write the baseline with the
|
|
3575
3619
|
# three pending columns NULL, so a cancelled candidate cannot linger.
|
|
3620
|
+
# The baseline's observation is this one unless a cancellation kept
|
|
3621
|
+
# the higher prior baseline.
|
|
3622
|
+
kept_prior = (prior_state is not None
|
|
3623
|
+
and float(decision.baseline_pct) != float(new_pct))
|
|
3576
3624
|
_write_five_hour_source_state(
|
|
3577
3625
|
conn, account_key, five_hour_window_key, source,
|
|
3578
3626
|
baseline_pct=decision.baseline_pct,
|
|
3627
|
+
baseline_observation_id=(
|
|
3628
|
+
None if not v2 else
|
|
3629
|
+
prior_state[4] if kept_prior else observation_id),
|
|
3630
|
+
baseline_generation=generation,
|
|
3579
3631
|
)
|
|
3580
3632
|
# FIVE_HOUR_HOLD writes nothing: either the arming observation replayed
|
|
3581
3633
|
# and must leave the state armed, or the reading neither raises the
|
|
@@ -3583,6 +3635,164 @@ def _step_five_hour_source_state(conn, account_key, five_hour_window_key,
|
|
|
3583
3635
|
return decision, prior_state
|
|
3584
3636
|
|
|
3585
3637
|
|
|
3638
|
+
#: The no-credit sentinel of the five-hour credit generation (spec 4.9).
|
|
3639
|
+
FIVE_HOUR_NO_CREDIT_GENERATION = "none"
|
|
3640
|
+
#: The source-key prefix of a credit's causal facts in the disposable
|
|
3641
|
+
#: confirmation state (see `_record_five_hour_credit_facts`). No real
|
|
3642
|
+
#: contributor source carries it: the pipeline files such a source as
|
|
3643
|
+
#: `FIVE_HOUR_UNKNOWN_SOURCE`.
|
|
3644
|
+
FIVE_HOUR_CREDIT_FACTS_PREFIX = "#credit|"
|
|
3645
|
+
|
|
3646
|
+
|
|
3647
|
+
def five_hour_credit_generation(conn, five_hour_window_key, *,
|
|
3648
|
+
account_key) -> str:
|
|
3649
|
+
"""The account and window's current five-hour credit generation
|
|
3650
|
+
(#797/#823/#840 spec 4.9, D3/D11): ``"<event_id>@<revision>"`` for the
|
|
3651
|
+
selected effective credit, else ``"none"``.
|
|
3652
|
+
|
|
3653
|
+
The credit is the one `resolve_five_hour_reset_event` selects by logical
|
|
3654
|
+
order (effective instant, detection instant, stable journal identity),
|
|
3655
|
+
never by rowid, so a rebuild that renumbers rows cannot change it. The
|
|
3656
|
+
event id is the harvest's natural key ``fhc:<account>:<window>:<effective>``
|
|
3657
|
+
and the revision is a digest of the row's accounting content, so a
|
|
3658
|
+
correction that rewrites the credit, or a tombstone that removes it,
|
|
3659
|
+
gives a different generation and makes every state that depends on the
|
|
3660
|
+
old one obsolete. A database error propagates."""
|
|
3661
|
+
import _lib_journal
|
|
3662
|
+
|
|
3663
|
+
event = resolve_five_hour_reset_event(
|
|
3664
|
+
conn, five_hour_window_key, account_key=account_key)
|
|
3665
|
+
if isinstance(event, NoFiveHourResetEvent):
|
|
3666
|
+
return FIVE_HOUR_NO_CREDIT_GENERATION
|
|
3667
|
+
row = conn.execute(
|
|
3668
|
+
"SELECT detected_at_utc, prior_percent, post_percent, "
|
|
3669
|
+
" effective_reset_at_utc "
|
|
3670
|
+
" FROM five_hour_reset_events WHERE id = ?",
|
|
3671
|
+
(event.row_id,),
|
|
3672
|
+
).fetchone()
|
|
3673
|
+
identity = _lib_journal.evt_id(
|
|
3674
|
+
"fhc", account_key, int(five_hour_window_key),
|
|
3675
|
+
event.effective_reset_at_utc)
|
|
3676
|
+
return f"{identity}@{_five_hour_credit_revision(row)}"
|
|
3677
|
+
|
|
3678
|
+
|
|
3679
|
+
def _five_hour_credit_revision(row) -> str:
|
|
3680
|
+
"""A five-hour credit row's content revision: its accounting payload
|
|
3681
|
+
(detection instant, both ends, effective instant), digested."""
|
|
3682
|
+
import hashlib
|
|
3683
|
+
|
|
3684
|
+
return hashlib.sha256(json.dumps(
|
|
3685
|
+
[str(row[0]), round(float(row[1]), 6), round(float(row[2]), 6),
|
|
3686
|
+
str(row[3])], separators=(",", ":")).encode("utf-8")).hexdigest()[:16]
|
|
3687
|
+
|
|
3688
|
+
|
|
3689
|
+
def _position_epoch(text) -> "float | None":
|
|
3690
|
+
try:
|
|
3691
|
+
return parse_iso_datetime(text, "five_hour_state.position").timestamp()
|
|
3692
|
+
except (TypeError, ValueError, AttributeError):
|
|
3693
|
+
return None
|
|
3694
|
+
|
|
3695
|
+
|
|
3696
|
+
def _rebase_five_hour_sources(conn, account_key, five_hour_window_key,
|
|
3697
|
+
confirming_source, *, arming_position,
|
|
3698
|
+
old_generation, new_generation):
|
|
3699
|
+
"""Carry every OTHER contributor of the window across a v2 credit
|
|
3700
|
+
(#797/#823/#840 spec 4.9, D11).
|
|
3701
|
+
|
|
3702
|
+
A contributor whose pending descent was armed strictly AFTER the
|
|
3703
|
+
confirming source's arming observation observed its low after the credit
|
|
3704
|
+
happened, so that reading is independently observed post-credit growth:
|
|
3705
|
+
it becomes the contributor's baseline in the new generation (the
|
|
3706
|
+
observation identity carried with it) and the obsolete descent itself is
|
|
3707
|
+
retired, never relabelled. D11's trace: A arms `20 -> 5`, B observes a
|
|
3708
|
+
genuine 10, A confirms; B's `20 -> 10` is retired, its 10 is kept, and a
|
|
3709
|
+
later `10 -> 0` is detected. Every other contributor state is left to the
|
|
3710
|
+
generation rule, which retires it at that contributor's next
|
|
3711
|
+
observation; a state recorded without a generation is stamped with the
|
|
3712
|
+
old one first, so the rule reaches it too. An unknown or equal position
|
|
3713
|
+
is never "after"."""
|
|
3714
|
+
rows = conn.execute(
|
|
3715
|
+
"SELECT source, baseline_pct, pending_low_pct, pending_at_utc, "
|
|
3716
|
+
" pending_observation_id, baseline_observation_id, "
|
|
3717
|
+
" baseline_generation, pending_generation, pending_position "
|
|
3718
|
+
" FROM five_hour_credit_confirmation_state "
|
|
3719
|
+
" WHERE account_key = ? AND five_hour_window_key = ?",
|
|
3720
|
+
(account_key, int(five_hour_window_key)),
|
|
3721
|
+
).fetchall()
|
|
3722
|
+
armed_at = _position_epoch(arming_position)
|
|
3723
|
+
for row in rows:
|
|
3724
|
+
source = row[0]
|
|
3725
|
+
if (source == confirming_source
|
|
3726
|
+
or str(source).startswith(FIVE_HOUR_CREDIT_FACTS_PREFIX)):
|
|
3727
|
+
continue
|
|
3728
|
+
position = _position_epoch(row[8])
|
|
3729
|
+
if (row[2] is not None
|
|
3730
|
+
and row[7] in (old_generation, None)
|
|
3731
|
+
and armed_at is not None and position is not None
|
|
3732
|
+
and position > armed_at):
|
|
3733
|
+
_write_five_hour_source_state(
|
|
3734
|
+
conn, account_key, five_hour_window_key, source,
|
|
3735
|
+
baseline_pct=float(row[2]),
|
|
3736
|
+
baseline_observation_id=row[4],
|
|
3737
|
+
baseline_generation=new_generation,
|
|
3738
|
+
)
|
|
3739
|
+
elif row[6] is None or (row[2] is not None and row[7] is None):
|
|
3740
|
+
_write_five_hour_source_state(
|
|
3741
|
+
conn, account_key, five_hour_window_key, source,
|
|
3742
|
+
baseline_pct=float(row[1]),
|
|
3743
|
+
pending_low_pct=row[2], pending_at_utc=row[3],
|
|
3744
|
+
pending_observation_id=row[4],
|
|
3745
|
+
baseline_observation_id=row[5],
|
|
3746
|
+
baseline_generation=row[6] or old_generation,
|
|
3747
|
+
pending_generation=(
|
|
3748
|
+
None if row[2] is None else row[7] or old_generation),
|
|
3749
|
+
pending_position=row[8],
|
|
3750
|
+
)
|
|
3751
|
+
|
|
3752
|
+
|
|
3753
|
+
def _record_five_hour_credit_facts(conn, account_key, five_hour_window_key,
|
|
3754
|
+
confirming_source, *, prior_pct, post_pct,
|
|
3755
|
+
armed_state, old_generation,
|
|
3756
|
+
new_generation):
|
|
3757
|
+
"""Retain one v2 credit's causal facts beside the confirmation state
|
|
3758
|
+
(#797/#823/#840 spec 4.4, 4.9): which source confirmed it, the
|
|
3759
|
+
observation that established that source's baseline and the arming
|
|
3760
|
+
observation. The status line resolves a five-hour credit's causal low
|
|
3761
|
+
through them (a `statusline` credit, or an `api` credit whose baseline
|
|
3762
|
+
and arming observations are provider samples); the confirmation state's
|
|
3763
|
+
own row is replaced at the confirmation, so without this nothing would
|
|
3764
|
+
say which observations a credit came from.
|
|
3765
|
+
|
|
3766
|
+
It is disposable exactly like the state it sits in: a rebuild drops it,
|
|
3767
|
+
and a credit without facts is resolved as unknown, which invalidates and
|
|
3768
|
+
so never refuses. The row is keyed by its ordinal in the window and the
|
|
3769
|
+
confirming source, so the writer's trailing DELETE retires the same key
|
|
3770
|
+
of earlier windows and the table stays proportional to live state. It
|
|
3771
|
+
stores the credit's two ends in the baseline and pending columns, the
|
|
3772
|
+
arming observation and instant, the baseline observation, and the
|
|
3773
|
+
generation before (``baseline_generation``) and after
|
|
3774
|
+
(``pending_generation``, which binds it to the credit's identity and
|
|
3775
|
+
revision) the credit."""
|
|
3776
|
+
count = conn.execute(
|
|
3777
|
+
"SELECT COUNT(*) FROM five_hour_credit_confirmation_state "
|
|
3778
|
+
" WHERE account_key = ? AND five_hour_window_key = ? "
|
|
3779
|
+
" AND source LIKE '#credit|%'",
|
|
3780
|
+
(account_key, int(five_hour_window_key)),
|
|
3781
|
+
).fetchone()[0]
|
|
3782
|
+
_write_five_hour_source_state(
|
|
3783
|
+
conn, account_key, five_hour_window_key,
|
|
3784
|
+
f"{FIVE_HOUR_CREDIT_FACTS_PREFIX}{int(count) + 1:02d}|{confirming_source}",
|
|
3785
|
+
baseline_pct=float(prior_pct),
|
|
3786
|
+
pending_low_pct=float(post_pct),
|
|
3787
|
+
pending_at_utc=None if armed_state is None else armed_state[2],
|
|
3788
|
+
pending_observation_id=None if armed_state is None else armed_state[3],
|
|
3789
|
+
baseline_observation_id=None if armed_state is None else armed_state[4],
|
|
3790
|
+
baseline_generation=old_generation,
|
|
3791
|
+
pending_generation=new_generation,
|
|
3792
|
+
pending_position=None if armed_state is None else armed_state[7],
|
|
3793
|
+
)
|
|
3794
|
+
|
|
3795
|
+
|
|
3586
3796
|
# ``CreditPlan`` / ``_parse_credit_at`` / ``_build_credit_plan`` now live in
|
|
3587
3797
|
# ``bin/_lib_credit.py`` (#279 S4 F1); re-imported at module top so the
|
|
3588
3798
|
# ``bin/cctally`` re-exports and this module's own callers
|
|
@@ -3804,9 +4014,11 @@ def _credit_retirement_bands(conn, *, week_start_date, week_start_at,
|
|
|
3804
4014
|
|
|
3805
4015
|
#834 S1 (#835). `_doomed_snapshot_rows` names the rows a firing credit is
|
|
3806
4016
|
about to remove; this names, for a week that has already been credited, the
|
|
3807
|
-
(floor, retired value) pairs a stored row can still match.
|
|
3808
|
-
|
|
3809
|
-
|
|
4017
|
+
(floor, retired value) pairs a stored row can still match. #835 used the
|
|
4018
|
+
retired values as a read-side band; #840 (spec section 4.8) removed that
|
|
4019
|
+
band, so the one remaining reader is `_latest_credit_floor_instant`, which
|
|
4020
|
+
takes only the floors: a stored value is read as stored, and a poisoned one
|
|
4021
|
+
is corrected by `db rederive`'s reviewed `retract`.
|
|
3810
4022
|
|
|
3811
4023
|
The two legs are scoped exactly the way `_reset_aware_floor`
|
|
3812
4024
|
(bin/_cctally_core.py) scopes them, and the two must stay in step: a
|
|
@@ -3875,8 +4087,7 @@ def _latest_credit_floor_instant(bands):
|
|
|
3875
4087
|
drift apart, and so a caller cannot accidentally take the FIRST floor, which
|
|
3876
4088
|
would admit rows an intervening credit already invalidated.
|
|
3877
4089
|
|
|
3878
|
-
An unparseable floor contributes nothing
|
|
3879
|
-
`_weekly_value_is_retired_replica`, which skips one for the same reason."""
|
|
4090
|
+
An unparseable floor contributes nothing."""
|
|
3880
4091
|
latest = None
|
|
3881
4092
|
for floor_at, _retired, _inclusive in bands:
|
|
3882
4093
|
try:
|
|
@@ -3888,71 +4099,6 @@ def _latest_credit_floor_instant(bands):
|
|
|
3888
4099
|
return latest
|
|
3889
4100
|
|
|
3890
4101
|
|
|
3891
|
-
def _weekly_value_is_retired_replica(bands, *, captured_at_utc, weekly_percent):
|
|
3892
|
-
"""True when a stored weekly value is one a credit in `bands` retired.
|
|
3893
|
-
|
|
3894
|
-
#834 S1 (#835). The same band and floor predicate the stale-replica DELETE
|
|
3895
|
-
applies, applied as a READ filter instead of as a deletion: captured at or
|
|
3896
|
-
after the credit's floor, and within 1.0 point of the value that credit
|
|
3897
|
-
retired.
|
|
3898
|
-
|
|
3899
|
-
Instants are compared as PARSED moments rather than as text, because the two
|
|
3900
|
-
sides carry mixed offset spellings (`Z` on a capture stamp, `+00:00` on a
|
|
3901
|
-
floor) — the same reason every SQL site wraps both sides in `unixepoch()`.
|
|
3902
|
-
|
|
3903
|
-
AN UNPARSEABLE CAPTURE STAMP IS TREATED AS NOT RETIRED HERE, and what that
|
|
3904
|
-
means for the read depends on the CALLER. There are three, and the floor bound
|
|
3905
|
-
belongs to exactly one of them.
|
|
3906
|
-
|
|
3907
|
-
`_latest_seven_day_and_window` (bin/_cctally_five_hour.py) carries a floor bound
|
|
3908
|
-
ahead of this predicate, added in Gate A R1. That bound treats an unparseable
|
|
3909
|
-
stamp inside a credited week as pre-floor and ends the walk with `None`, which
|
|
3910
|
-
is the right disposition: a row that cannot be placed relative to the floor
|
|
3911
|
-
cannot be certified as able to state the week's current effective value. So
|
|
3912
|
-
there, a malformed row blanks the read.
|
|
3913
|
-
|
|
3914
|
-
`_load_five_hour_milestones` (bin/_cctally_five_hour.py, from tranche A) carries
|
|
3915
|
-
no such bound. An unparseable `snapshot_captured_at_utc` makes this function
|
|
3916
|
-
return `False` and the joined effective crossing value RENDERS. A correction of
|
|
3917
|
-
record: an earlier version of this docstring generalized R1's floor bound to
|
|
3918
|
-
every caller, and it holds for one.
|
|
3919
|
-
|
|
3920
|
-
`_credit_aware_block_weekly_axes` (bin/_cctally_five_hour.py) carries no floor
|
|
3921
|
-
bound either, but since Gate A R8 an unparseable instant never reaches this
|
|
3922
|
-
predicate from it: `_block_weekly_axis_weeks` parses every axis instant first —
|
|
3923
|
-
including `block_start_at`, which the start axis falls back to — and returns an
|
|
3924
|
-
EMPTY TUPLE when it cannot, which resolves no bands. (It returned `None` when
|
|
3925
|
-
this paragraph was written; R8 made it return a tuple of candidate weeks and R9
|
|
3926
|
-
added a third resolution pass, and both forms are falsy, so the caller's `if not
|
|
3927
|
-
week_ids` guard is unaffected. The stated value was simply stale.) The value
|
|
3928
|
-
renders for that reason rather than this one. Same outcome, different mechanism,
|
|
3929
|
-
and the mechanism matters because only one of the two is a deliberate decision.
|
|
3930
|
-
|
|
3931
|
-
What this function's own tolerance buys in every case is that a malformed stamp
|
|
3932
|
-
is never MISclassified as retired, which is what keeps the uncredited-week path
|
|
3933
|
-
byte-stable."""
|
|
3934
|
-
if weekly_percent is None or not bands:
|
|
3935
|
-
return False
|
|
3936
|
-
try:
|
|
3937
|
-
captured = parse_iso_datetime(captured_at_utc, "captured_at").timestamp()
|
|
3938
|
-
except (ValueError, TypeError):
|
|
3939
|
-
return False
|
|
3940
|
-
for floor_at, retired, inclusive in bands:
|
|
3941
|
-
try:
|
|
3942
|
-
floor = parse_iso_datetime(floor_at, "credit_floor").timestamp()
|
|
3943
|
-
except (ValueError, TypeError):
|
|
3944
|
-
continue
|
|
3945
|
-
if captured < floor:
|
|
3946
|
-
continue
|
|
3947
|
-
delta = abs(float(weekly_percent) - retired)
|
|
3948
|
-
# The band's comparison belongs to the credit that set it: the automatic
|
|
3949
|
-
# path is inclusive, `record-credit` is strict. See
|
|
3950
|
-
# `_doomed_snapshot_rows` for why those two differ.
|
|
3951
|
-
if (delta <= 1.0) if inclusive else (delta < 1.0):
|
|
3952
|
-
return True
|
|
3953
|
-
return False
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
4102
|
def _fire_in_place_credit(conn, week_start_date, cur_end_canon, weekly_percent,
|
|
3957
4103
|
*, observed_pre_credit_pct, effective_dt,
|
|
3958
4104
|
as_of=None, commit=True, ctx=None,
|
|
@@ -4189,7 +4335,8 @@ def detect_reset_and_credit(conn, *, week_start_date, week_end_at,
|
|
|
4189
4335
|
ctx=None, account_key=_lib_accounts.UNATTRIBUTED,
|
|
4190
4336
|
origin_observation_id=None, capture_at=None,
|
|
4191
4337
|
source=None, hold_weekly_axis=False,
|
|
4192
|
-
outcomes_allowed=True, fold_policy=0
|
|
4338
|
+
outcomes_allowed=True, fold_policy=0,
|
|
4339
|
+
detector_policy="historical"):
|
|
4193
4340
|
"""Detect + record weekly and 5h reset/credit artifacts for one usage
|
|
4194
4341
|
observation (extracted from ``cmd_record_usage``; DB journal redesign
|
|
4195
4342
|
§5.2.3).
|
|
@@ -4277,6 +4424,15 @@ def detect_reset_and_credit(conn, *, week_start_date, week_end_at,
|
|
|
4277
4424
|
records every doomed row in its one ``suppression`` list, exactly as
|
|
4278
4425
|
the historical policy writes it, so the previous release folds it,
|
|
4279
4426
|
and the current applier replays it with the same split.
|
|
4427
|
+
- ``detector_policy`` (#797/#823/#840 spec 4.9, default
|
|
4428
|
+
``"historical"``): the detector revision the raw observation recorded,
|
|
4429
|
+
as `_cctally_journal.detector_policy_of` reads it. ``"v2"`` decides
|
|
4430
|
+
five-hour credits by the per-account, per-window credit generation
|
|
4431
|
+
instead of the ``(prior_percent, post_percent)`` pair check. The
|
|
4432
|
+
default keeps every direct caller, and every observation an earlier
|
|
4433
|
+
release journaled, on the historical detector. The pipeline never
|
|
4434
|
+
calls this for an ``"unsupported"`` revision (the step is withheld),
|
|
4435
|
+
and passes a projection-filled axis as not observed (D6).
|
|
4280
4436
|
"""
|
|
4281
4437
|
c = _cctally()
|
|
4282
4438
|
now_utc = _as_of_or_command(as_of)
|
|
@@ -4560,6 +4716,16 @@ def detect_reset_and_credit(conn, *, week_start_date, week_end_at,
|
|
|
4560
4716
|
# SECOND reading would become the baseline and a descent
|
|
4561
4717
|
# between them would be invisible.
|
|
4562
4718
|
five_hour_source = source or FIVE_HOUR_UNKNOWN_SOURCE
|
|
4719
|
+
if five_hour_source.startswith(FIVE_HOUR_CREDIT_FACTS_PREFIX):
|
|
4720
|
+
five_hour_source = FIVE_HOUR_UNKNOWN_SOURCE
|
|
4721
|
+
# #797/#823/#840 spec 4.9 (D3, D11): the v2 detector decides
|
|
4722
|
+
# against the window's current credit generation, read in this
|
|
4723
|
+
# same transaction, so the confirmation rechecks it.
|
|
4724
|
+
v2_detector = detector_policy == "v2"
|
|
4725
|
+
generation = (
|
|
4726
|
+
five_hour_credit_generation(
|
|
4727
|
+
conn, five_hour_window_key, account_key=account_key)
|
|
4728
|
+
if v2_detector else None)
|
|
4563
4729
|
src_decision, src_state = _step_five_hour_source_state(
|
|
4564
4730
|
conn, account_key, five_hour_window_key, five_hour_source,
|
|
4565
4731
|
new_pct=float(five_hour_percent),
|
|
@@ -4567,6 +4733,8 @@ def detect_reset_and_credit(conn, *, week_start_date, week_end_at,
|
|
|
4567
4733
|
drop_threshold=c._FIVE_HOUR_RESET_PCT_DROP_THRESHOLD,
|
|
4568
4734
|
window_live=window_live,
|
|
4569
4735
|
now_iso=now_utc.isoformat(timespec="seconds"),
|
|
4736
|
+
generation=generation,
|
|
4737
|
+
capture_at=capture_dt.isoformat(timespec="seconds"),
|
|
4570
4738
|
)
|
|
4571
4739
|
if src_decision.action == FIVE_HOUR_CONFIRM and not outcomes_allowed:
|
|
4572
4740
|
# Quarantined (spec 5.6 item 2): no event, no stale-replica
|
|
@@ -4634,21 +4802,34 @@ def detect_reset_and_credit(conn, *, week_start_date, week_end_at,
|
|
|
4634
4802
|
# `post_5h_pct`, not the confirming tick's percent —
|
|
4635
4803
|
# comparing an armed low against a confirming percent
|
|
4636
4804
|
# answers no question at all.
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4805
|
+
#
|
|
4806
|
+
# #797/#823/#840 spec 4.9 (D3): the v2 detector replaces
|
|
4807
|
+
# the pair check with causal retry identity plus the
|
|
4808
|
+
# credit generation. A credit is now confirmed only from a
|
|
4809
|
+
# descent armed in the current generation, so a replay of
|
|
4810
|
+
# one descent, or a second source's copy of it, can no
|
|
4811
|
+
# longer reach this point, while a genuine second
|
|
4812
|
+
# `20 -> 5` in a distinct slot is a new credit the pair
|
|
4813
|
+
# check would have swallowed. The historical detector
|
|
4814
|
+
# keeps the pair check exactly.
|
|
4815
|
+
if v2_detector:
|
|
4816
|
+
is_dup = False
|
|
4817
|
+
else:
|
|
4818
|
+
most_recent = conn.execute(
|
|
4819
|
+
"SELECT prior_percent, post_percent "
|
|
4820
|
+
" FROM five_hour_reset_events "
|
|
4821
|
+
" WHERE five_hour_window_key = ? "
|
|
4822
|
+
" AND account_key = ? "
|
|
4823
|
+
" ORDER BY id DESC LIMIT 1",
|
|
4824
|
+
(int(five_hour_window_key), account_key),
|
|
4825
|
+
).fetchone()
|
|
4826
|
+
is_dup = (
|
|
4827
|
+
most_recent is not None
|
|
4828
|
+
and round(prior_5h_pct, 1)
|
|
4829
|
+
== round(float(most_recent["prior_percent"]), 1)
|
|
4830
|
+
and round(post_5h_pct, 1)
|
|
4831
|
+
== round(float(most_recent["post_percent"]), 1)
|
|
4832
|
+
)
|
|
4652
4833
|
# 10-min floor (spec §2.3 — bounded
|
|
4653
4834
|
# stacked-credit resolution; one event per
|
|
4654
4835
|
# 10-min slot per block). Resolved BEFORE
|
|
@@ -4911,10 +5092,37 @@ def detect_reset_and_credit(conn, *, week_start_date, week_end_at,
|
|
|
4911
5092
|
# armed so the next same-source reading below the
|
|
4912
5093
|
# baseline re-confirms and re-runs the idempotent
|
|
4913
5094
|
# pivots, rather than losing the credit outright.
|
|
5095
|
+
new_generation = None
|
|
5096
|
+
if v2_detector:
|
|
5097
|
+
# The generation this credit established (unchanged
|
|
5098
|
+
# when the ten-minute slot absorbed it). When it moved,
|
|
5099
|
+
# every other contributor is carried across it and the
|
|
5100
|
+
# credit's causal facts are retained.
|
|
5101
|
+
new_generation = five_hour_credit_generation(
|
|
5102
|
+
conn, five_hour_window_key,
|
|
5103
|
+
account_key=account_key)
|
|
5104
|
+
if new_generation != generation:
|
|
5105
|
+
_rebase_five_hour_sources(
|
|
5106
|
+
conn, account_key, five_hour_window_key,
|
|
5107
|
+
five_hour_source,
|
|
5108
|
+
arming_position=(
|
|
5109
|
+
src_state[7] or src_state[2]
|
|
5110
|
+
if src_state is not None else None),
|
|
5111
|
+
old_generation=generation,
|
|
5112
|
+
new_generation=new_generation)
|
|
5113
|
+
_record_five_hour_credit_facts(
|
|
5114
|
+
conn, account_key, five_hour_window_key,
|
|
5115
|
+
five_hour_source, prior_pct=prior_5h_pct,
|
|
5116
|
+
post_pct=post_5h_pct, armed_state=src_state,
|
|
5117
|
+
old_generation=generation,
|
|
5118
|
+
new_generation=new_generation)
|
|
4914
5119
|
_write_five_hour_source_state(
|
|
4915
5120
|
conn, account_key, five_hour_window_key,
|
|
4916
5121
|
five_hour_source,
|
|
4917
5122
|
baseline_pct=float(five_hour_percent),
|
|
5123
|
+
baseline_observation_id=(
|
|
5124
|
+
origin_observation_id if v2_detector else None),
|
|
5125
|
+
baseline_generation=new_generation,
|
|
4918
5126
|
)
|
|
4919
5127
|
except (sqlite3.DatabaseError, ValueError, TypeError) as exc:
|
|
4920
5128
|
# Exception discipline (6c-gate P1): on the ingest path
|
|
@@ -6182,6 +6390,16 @@ def record_usage_bound(
|
|
|
6182
6390
|
raw["five_hour_percent"] = five_hour_percent
|
|
6183
6391
|
if five_hour_resets_at_str is not None:
|
|
6184
6392
|
raw["five_hour_resets_at"] = five_hour_resets_at_str
|
|
6393
|
+
# #797/#823/#840 spec 4.3 D8: the request-start instant `q` of the OAuth
|
|
6394
|
+
# fetch publishing this reading, an additive raw key the previous release
|
|
6395
|
+
# reads past with `.get`. Only an api publication whose fetch recorded `q`
|
|
6396
|
+
# carries it; nothing else — a status-line reading, a direct
|
|
6397
|
+
# `record-usage`, a frozen plan's recovery — ever gains one.
|
|
6398
|
+
request_started = getattr(args, "oauth_request_started_at", None)
|
|
6399
|
+
if (isinstance(request_started, str) and publication is None
|
|
6400
|
+
and raw["source"] == "api"):
|
|
6401
|
+
import _lib_oauth_samples
|
|
6402
|
+
raw[_lib_oauth_samples.REQUEST_STARTED_KEY] = request_started
|
|
6185
6403
|
# #825/#832 spec 4.7 / 5.1: the binding and the plan's stable publication
|
|
6186
6404
|
# identity live in the RAW observation payload (never in a known event
|
|
6187
6405
|
# payload). The previous release reads this payload by `.get`, so the extra
|
|
@@ -6207,6 +6425,13 @@ def record_usage_bound(
|
|
|
6207
6425
|
raw[_jr.FOLD_POLICY_KEY] = _jr.CURRENT_FOLD_POLICY
|
|
6208
6426
|
elif _jr.PLAN_FOLD_POLICY_KEY in publication:
|
|
6209
6427
|
raw[_jr.FOLD_POLICY_KEY] = publication[_jr.PLAN_FOLD_POLICY_KEY]
|
|
6428
|
+
# #797/#823/#840 spec 4.9 (D12): the detector revision, recorded the same
|
|
6429
|
+
# way. A plan frozen before it carries none, so its recovery publishes an
|
|
6430
|
+
# observation the historical detector decides, byte-identical to before.
|
|
6431
|
+
if publication is None:
|
|
6432
|
+
raw[_jr.DETECTOR_POLICY_KEY] = _jr.DETECTOR_POLICY
|
|
6433
|
+
elif _jr.PLAN_DETECTOR_POLICY_KEY in publication:
|
|
6434
|
+
raw[_jr.DETECTOR_POLICY_KEY] = publication[_jr.PLAN_DETECTOR_POLICY_KEY]
|
|
6210
6435
|
import _lib_accounts
|
|
6211
6436
|
# Observe-and-stamp (#341, spec §1): resolve the active Claude account from
|
|
6212
6437
|
# ~/.claude.json (stable-read, mtime-cached) and stamp the usage obs with it.
|
|
@@ -7842,6 +8067,59 @@ def _five_hour_saved_from_fold(fold, *, snapshot_id, capture_at,
|
|
|
7842
8067
|
}
|
|
7843
8068
|
|
|
7844
8069
|
|
|
8070
|
+
def _detector_observation_mask(payload, detector_policy, *, account_key,
|
|
8071
|
+
five_hour) -> "tuple[bool, bool] | None":
|
|
8072
|
+
"""``(observe_weekly, observe_five_hour)`` for one raw observation, or
|
|
8073
|
+
None when its detector step is withheld (#797/#823/#840 spec 4.9, D6).
|
|
8074
|
+
|
|
8075
|
+
The historical detector sees every axis, as every earlier release did. A
|
|
8076
|
+
v2 observation that is not a status-line publication (a direct
|
|
8077
|
+
`record-usage`, an OAuth reading) is a reading of every axis it carries.
|
|
8078
|
+
A v2 publication's validated provenance decides per axis: a `projection`
|
|
8079
|
+
axis was filled from the store and is not a new reading of its source,
|
|
8080
|
+
while `candidate` and `retained_low` axes are. Provenance that does not
|
|
8081
|
+
classify a published axis for this account cannot say which axes are
|
|
8082
|
+
readings, so the step is withheld."""
|
|
8083
|
+
if detector_policy == "unsupported":
|
|
8084
|
+
return None
|
|
8085
|
+
if detector_policy == "historical" or "publication_id" not in payload:
|
|
8086
|
+
return True, True
|
|
8087
|
+
provenance = payload.get("publication")
|
|
8088
|
+
if not isinstance(provenance, dict):
|
|
8089
|
+
return None
|
|
8090
|
+
observed = {}
|
|
8091
|
+
for axis, present in (("sevenDay", True), ("fiveHour", five_hour)):
|
|
8092
|
+
if not present:
|
|
8093
|
+
observed[axis] = False
|
|
8094
|
+
continue
|
|
8095
|
+
source = provenance.get(axis)
|
|
8096
|
+
if (not isinstance(source, dict)
|
|
8097
|
+
or source.get("account") != account_key
|
|
8098
|
+
or source.get("kind") not in ("candidate", "projection",
|
|
8099
|
+
"retained_low")):
|
|
8100
|
+
return None
|
|
8101
|
+
observed[axis] = source["kind"] != "projection"
|
|
8102
|
+
return observed["sevenDay"], observed["fiveHour"]
|
|
8103
|
+
|
|
8104
|
+
|
|
8105
|
+
def _detect_observation(c, conn, *, observe, detector_policy, **kwargs):
|
|
8106
|
+
"""The shared detector over one observation's observed axes (spec 4.9).
|
|
8107
|
+
A projection-filled weekly axis takes the held branch (no weekly reset or
|
|
8108
|
+
credit detection at all, the five-hour detector still runs); a
|
|
8109
|
+
projection-filled five-hour axis is not passed, so the whole source-local
|
|
8110
|
+
step — baseline, arming, confirmation, cancellation, generation change
|
|
8111
|
+
and pruning — is skipped for it. The filled values stay available to the
|
|
8112
|
+
fold, which the caller runs on the observation unchanged."""
|
|
8113
|
+
observe_weekly, observe_five_hour = observe
|
|
8114
|
+
if not observe_weekly:
|
|
8115
|
+
kwargs["hold_weekly_axis"] = True
|
|
8116
|
+
if not observe_five_hour:
|
|
8117
|
+
kwargs["five_hour_window_key"] = None
|
|
8118
|
+
kwargs["five_hour_percent"] = None
|
|
8119
|
+
c.detect_reset_and_credit(
|
|
8120
|
+
conn, commit=False, detector_policy=detector_policy, **kwargs)
|
|
8121
|
+
|
|
8122
|
+
|
|
7845
8123
|
def _pipeline_claude_usage(ctx, rec):
|
|
7846
8124
|
"""Obs-derivation pipeline hook (spec §5.2 step 4b) — the ingest-cycle
|
|
7847
8125
|
transplant of ``cmd_record_usage``'s derivation body.
|
|
@@ -7909,6 +8187,15 @@ def _pipeline_claude_usage(ctx, rec):
|
|
|
7909
8187
|
import _lib_accounts
|
|
7910
8188
|
account_key = rec.get("account") or _lib_accounts.UNATTRIBUTED
|
|
7911
8189
|
|
|
8190
|
+
# #797/#823/#840 spec 4.3 D8: the provider sample, projected from the RAW
|
|
8191
|
+
# record before any detection or fold decision, so equal readings the fold
|
|
8192
|
+
# deduplicates still each produce one, and neither the five-hour
|
|
8193
|
+
# adjustment nor a credit's suppression or retirement can alter it. The
|
|
8194
|
+
# same call serves live ingest and both scratch replays; the apply-only
|
|
8195
|
+
# rebuild offers its read pass to the same projection.
|
|
8196
|
+
import _cctally_oauth_samples
|
|
8197
|
+
_cctally_oauth_samples.project_record(conn, rec)
|
|
8198
|
+
|
|
7912
8199
|
# Week boundaries from resets_at (cmd_record_usage canonicalization).
|
|
7913
8200
|
# Normalize before deriving the date keys as well as the ISO overrides:
|
|
7914
8201
|
# rounding a 23:30–23:59 boundary crosses midnight, and letting
|
|
@@ -7976,33 +8263,37 @@ def _pipeline_claude_usage(ctx, rec):
|
|
|
7976
8263
|
# ordinary recording below continues (C3).
|
|
7977
8264
|
import _cctally_quarantine
|
|
7978
8265
|
gate = _cctally_quarantine.quarantine_gate(ctx, account_key)
|
|
7979
|
-
|
|
7980
|
-
|
|
7981
|
-
|
|
7982
|
-
|
|
7983
|
-
|
|
7984
|
-
|
|
7985
|
-
|
|
7986
|
-
|
|
7987
|
-
|
|
7988
|
-
|
|
7989
|
-
|
|
7990
|
-
|
|
7991
|
-
|
|
7992
|
-
|
|
7993
|
-
|
|
7994
|
-
|
|
7995
|
-
|
|
7996
|
-
|
|
7997
|
-
|
|
7998
|
-
|
|
7999
|
-
|
|
8000
|
-
|
|
8001
|
-
|
|
8002
|
-
|
|
8003
|
-
|
|
8004
|
-
|
|
8005
|
-
|
|
8266
|
+
# #797/#823/#840 spec 4.9 (D6, D12): the detector revision this raw
|
|
8267
|
+
# observation recorded, and which of its axes are readings of its source.
|
|
8268
|
+
# An unsupported or malformed revision, or a v2 publication whose
|
|
8269
|
+
# provenance does not classify its axes, withholds the whole detector step
|
|
8270
|
+
# (an outcome may always be withheld); ordinary recording below continues.
|
|
8271
|
+
detector_policy = jr.detector_policy_of(payload)
|
|
8272
|
+
observe = _detector_observation_mask(
|
|
8273
|
+
payload, detector_policy, account_key=account_key,
|
|
8274
|
+
five_hour=five_hour_percent is not None)
|
|
8275
|
+
if observe is not None:
|
|
8276
|
+
_detect_observation(
|
|
8277
|
+
c, conn, observe=observe, detector_policy=detector_policy,
|
|
8278
|
+
week_start_date=week_start_date, week_end_at=week_end_at,
|
|
8279
|
+
weekly_percent=weekly_percent,
|
|
8280
|
+
five_hour_window_key=five_hour_window_key,
|
|
8281
|
+
five_hour_percent=five_hour_percent, as_of=as_of, ctx=ctx,
|
|
8282
|
+
account_key=account_key,
|
|
8283
|
+
# #750 S3: the raw journal id of the line this derivation came
|
|
8284
|
+
# from. The debounce ARM records it so a byte-identical replay of
|
|
8285
|
+
# the first zero cannot confirm itself.
|
|
8286
|
+
origin_observation_id=rec.get("id"),
|
|
8287
|
+
# The CAPTURE stamp, not `as_of`. It is what every
|
|
8288
|
+
# `effective_reset_at_utc` records, so live detection and the
|
|
8289
|
+
# backfill (which reads `weekly_usage_snapshots.captured_at_utc`)
|
|
8290
|
+
# agree about the same physical reset.
|
|
8291
|
+
capture_at=capture_at,
|
|
8292
|
+
# #769 S2 §3: the contributor discriminator for five-hour credit
|
|
8293
|
+
# confirmation. `payload.source`, never `rec["src"]`.
|
|
8294
|
+
source=source,
|
|
8295
|
+
hold_weekly_axis=hold_weekly_axis, outcomes_allowed=gate.allowed,
|
|
8296
|
+
fold_policy=fold_policy)
|
|
8006
8297
|
|
|
8007
8298
|
# 2. Accept/skip DECISION (clamp + dedup), made ONCE and journaled via the
|
|
8008
8299
|
# snapshot_accept evt (so replay never re-derives it — spec §5.3).
|