cctally 1.79.1 → 1.80.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_cache.py +148 -61
- package/bin/_cctally_config.py +23 -6
- package/bin/_cctally_core.py +342 -88
- package/bin/_cctally_dashboard.py +13 -7
- package/bin/_cctally_db.py +361 -36
- package/bin/_cctally_doctor.py +156 -0
- package/bin/_cctally_five_hour.py +19 -7
- package/bin/_cctally_forecast.py +43 -18
- package/bin/_cctally_journal.py +2148 -0
- package/bin/_cctally_milestones.py +116 -26
- package/bin/_cctally_parser.py +21 -3
- package/bin/_cctally_quota.py +195 -76
- package/bin/_cctally_record.py +1596 -1133
- package/bin/_cctally_statusline.py +13 -1
- package/bin/_cctally_store.py +505 -0
- package/bin/_cctally_sync_week.py +139 -2
- package/bin/_lib_doctor.py +209 -12
- package/bin/_lib_journal.py +188 -0
- package/bin/cctally +11 -1
- package/dashboard/static/assets/{index-Dc2u8c52.js → index-B3y14l1o.js} +49 -49
- package/dashboard/static/assets/index-Cc2ykqF_.css +1 -0
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +4 -1
- package/dashboard/static/assets/index-CrIlpAlQ.css +0 -1
package/bin/_cctally_quota.py
CHANGED
|
@@ -622,8 +622,18 @@ def _arming_row(conn: sqlite3.Connection, identity: QuotaWindowIdentity) -> sqli
|
|
|
622
622
|
|
|
623
623
|
def _activate_quota_rule(
|
|
624
624
|
conn: sqlite3.Connection, identity: QuotaWindowIdentity, fingerprint: str, now_iso: str,
|
|
625
|
+
*, journal_emit=None,
|
|
625
626
|
) -> tuple[bool, dt.datetime]:
|
|
626
|
-
"""Persist one identity's resolved rule boundary, returning (changed, at).
|
|
627
|
+
"""Persist one identity's resolved rule boundary, returning (changed, at).
|
|
628
|
+
|
|
629
|
+
Task 7 Item 5: on a genuine activation (a new/changed boundary) the arming
|
|
630
|
+
state is journaled — its ``activated_at_utc`` is a forward-only alert
|
|
631
|
+
boundary that must survive a stats.db rebuild so the reconcile honors it
|
|
632
|
+
(no historical re-fires, spec §5.3 "state"). ``journal_emit`` (set only on
|
|
633
|
+
the LIVE ingest-cycle path; ``None`` for a rebuild re-materialization, which
|
|
634
|
+
must not append) appends the ``quota_alert_arming`` evt. The row is still
|
|
635
|
+
written directly here; the evt is the additional durable record, and its
|
|
636
|
+
fold applier's natural-key upsert converges with this write on replay."""
|
|
627
637
|
row = _arming_row(conn, identity)
|
|
628
638
|
if row is not None and row["rule_fingerprint"] == fingerprint:
|
|
629
639
|
return False, _parse_utc(str(row["activated_at_utc"]), "activated_at_utc")
|
|
@@ -641,6 +651,8 @@ def _activate_quota_rule(
|
|
|
641
651
|
identity.observed_slot, identity.window_minutes, fingerprint, now_iso,
|
|
642
652
|
),
|
|
643
653
|
)
|
|
654
|
+
if journal_emit is not None:
|
|
655
|
+
journal_emit(identity, fingerprint, now_iso)
|
|
644
656
|
return True, _parse_utc(now_iso, "activated_at_utc")
|
|
645
657
|
|
|
646
658
|
|
|
@@ -705,7 +717,7 @@ def _quota_alert_payload(
|
|
|
705
717
|
def _evaluate_quota_alerts(
|
|
706
718
|
conn: sqlite3.Connection,
|
|
707
719
|
*, observations: tuple[QuotaObservation, ...], alert_eligible_roots: set[str],
|
|
708
|
-
now: dt.datetime, now_iso: str,
|
|
720
|
+
now: dt.datetime, now_iso: str, journal_emit=None,
|
|
709
721
|
) -> list[dict]:
|
|
710
722
|
"""Arm or claim quota alerts within the caller's stats transaction.
|
|
711
723
|
|
|
@@ -742,7 +754,8 @@ def _evaluate_quota_alerts(
|
|
|
742
754
|
identity, resolved, global_enabled=global_enabled,
|
|
743
755
|
quota_enabled=quota_enabled,
|
|
744
756
|
)
|
|
745
|
-
changed, activated_at = _activate_quota_rule(
|
|
757
|
+
changed, activated_at = _activate_quota_rule(
|
|
758
|
+
conn, identity, fingerprint, now_iso, journal_emit=journal_emit)
|
|
746
759
|
|
|
747
760
|
# Future evidence is never a threshold qualifier (including a first
|
|
748
761
|
# activation backfill). A later well-clocked observation creates the
|
|
@@ -812,6 +825,112 @@ def _evaluate_quota_alerts(
|
|
|
812
825
|
return queued
|
|
813
826
|
|
|
814
827
|
|
|
828
|
+
def _apply_quota_projection_rows(
|
|
829
|
+
conn, *, observations, active_roots, now, now_iso,
|
|
830
|
+
sink, alert_eligible_roots, journal_emit=None, holder=None,
|
|
831
|
+
):
|
|
832
|
+
"""Transaction-neutral quota projection apply (spec §5.3 "projection").
|
|
833
|
+
|
|
834
|
+
Materializes the interpreted ``quota_*`` rows on ``conn`` (no ``BEGIN`` /
|
|
835
|
+
``commit`` — the caller owns the transaction), queues alerts into ``sink``
|
|
836
|
+
(``None`` → none), and journals arming state changes via ``journal_emit``
|
|
837
|
+
(``None`` → none). ONE shared body drives both the live ingest ``codex_apply``
|
|
838
|
+
leg (``sink`` = the cycle's pending-alerts, ``journal_emit`` = the arming evt
|
|
839
|
+
emitter) AND the rebuild re-materialization pass (``sink=None``,
|
|
840
|
+
``journal_emit=None``, ``alert_eligible_roots`` empty) so the two never drift.
|
|
841
|
+
``holder`` (optional) captures the certificate signatures + result for the
|
|
842
|
+
live caller; rebuild passes ``None``."""
|
|
843
|
+
historic_roots = _historic_root_keys(conn)
|
|
844
|
+
roots_to_reconcile = active_roots | historic_roots
|
|
845
|
+
if not roots_to_reconcile:
|
|
846
|
+
return
|
|
847
|
+
generation = secrets.token_hex(16)
|
|
848
|
+
blocks = build_blocks(observations)
|
|
849
|
+
for block in blocks:
|
|
850
|
+
conn.execute(_BLOCK_UPSERT, _block_params(block, generation))
|
|
851
|
+
for milestone in percent_milestones(block):
|
|
852
|
+
conn.execute(
|
|
853
|
+
_MILESTONE_UPSERT,
|
|
854
|
+
_milestone_params(block, milestone, generation),
|
|
855
|
+
)
|
|
856
|
+
blocks_orphaned, milestones_orphaned = _orphan_unseen(
|
|
857
|
+
conn, roots_to_reconcile, generation, now_iso,
|
|
858
|
+
)
|
|
859
|
+
queued = _evaluate_quota_alerts(
|
|
860
|
+
conn, observations=observations,
|
|
861
|
+
alert_eligible_roots=alert_eligible_roots & active_roots,
|
|
862
|
+
now=now, now_iso=now_iso, journal_emit=journal_emit,
|
|
863
|
+
)
|
|
864
|
+
# The completion stamp is intentionally the final DML in the stats
|
|
865
|
+
# transaction. A pre-commit failure rolls all projection updates back;
|
|
866
|
+
# a retry sees the prior complete generation or rederives it.
|
|
867
|
+
for root_key in sorted(active_roots):
|
|
868
|
+
conn.execute(
|
|
869
|
+
"""INSERT INTO quota_projection_state
|
|
870
|
+
(source_root_key, generation, physical_signature, completed_at_utc)
|
|
871
|
+
VALUES (?,?,?,?)
|
|
872
|
+
ON CONFLICT(source_root_key) DO UPDATE SET
|
|
873
|
+
generation=excluded.generation,
|
|
874
|
+
physical_signature=excluded.physical_signature,
|
|
875
|
+
completed_at_utc=excluded.completed_at_utc""",
|
|
876
|
+
(root_key, generation, _signature(observations, root_key), now_iso),
|
|
877
|
+
)
|
|
878
|
+
if sink is not None:
|
|
879
|
+
# Set-then-dispatch: all claims committed with the cycle before the
|
|
880
|
+
# cycle's post-commit ALERT_DISPATCHER fires them (spec §5.2 step 6).
|
|
881
|
+
sink.extend(queued)
|
|
882
|
+
if holder is not None:
|
|
883
|
+
holder["signatures"] = {
|
|
884
|
+
root_key: _signature(observations, root_key) for root_key in active_roots
|
|
885
|
+
}
|
|
886
|
+
holder["result"] = QuotaProjectionResult(
|
|
887
|
+
generation=generation,
|
|
888
|
+
blocks_upserted=len(blocks),
|
|
889
|
+
milestones_upserted=sum(len(percent_milestones(b)) for b in blocks),
|
|
890
|
+
blocks_orphaned=blocks_orphaned,
|
|
891
|
+
milestones_orphaned=milestones_orphaned,
|
|
892
|
+
roots_stamped=len(active_roots),
|
|
893
|
+
alerts_dispatched=len(queued),
|
|
894
|
+
)
|
|
895
|
+
|
|
896
|
+
|
|
897
|
+
def rematerialize_quota_projection_for_rebuild(stats_conn, *, now=None) -> None:
|
|
898
|
+
"""Rebuild path (spec §5.4 / §5.3 "projection"): re-run the Codex quota
|
|
899
|
+
projection over the materialized cache.db ``quota_window_snapshots`` directly
|
|
900
|
+
onto the fresh rebuilt ``stats_conn``, side-effect-free.
|
|
901
|
+
|
|
902
|
+
Called by ``rebuild_stats_index`` AFTER the ``quota_alert_arming`` evts have
|
|
903
|
+
folded (order 45), so ``_evaluate_quota_alerts`` honors the replayed
|
|
904
|
+
activation boundary and re-fires nothing. NO alerts (``sink=None``), NO arming
|
|
905
|
+
journaling (``journal_emit=None``), NO alert-eligible roots. Opens no stats
|
|
906
|
+
connection of its own — writes on the caller's rebuild transaction. Uses the
|
|
907
|
+
SAME ``active_roots`` source as the live reconcile (``_cache_root_keys`` over
|
|
908
|
+
the cache) so the materialized projection matches live. A missing cache.db is
|
|
909
|
+
a clean no-op (the journal quota obs remain the durable source; a later
|
|
910
|
+
``cache-sync`` + reconcile re-materializes)."""
|
|
911
|
+
if now is None:
|
|
912
|
+
now = dt.datetime.now(UTC)
|
|
913
|
+
if now.tzinfo is None or now.utcoffset() is None:
|
|
914
|
+
raise ValueError("now must be timezone-aware")
|
|
915
|
+
now_iso = _utc_iso(now)
|
|
916
|
+
try:
|
|
917
|
+
cache = _cache_connection()
|
|
918
|
+
except (FileNotFoundError, sqlite3.Error):
|
|
919
|
+
return
|
|
920
|
+
try:
|
|
921
|
+
active_roots = _cache_root_keys(cache)
|
|
922
|
+
observations = load_codex_quota_observations(
|
|
923
|
+
source_root_keys=None, cache_conn=cache,
|
|
924
|
+
)
|
|
925
|
+
finally:
|
|
926
|
+
cache.close()
|
|
927
|
+
_apply_quota_projection_rows(
|
|
928
|
+
stats_conn, observations=observations, active_roots=active_roots,
|
|
929
|
+
now=now, now_iso=now_iso, sink=None,
|
|
930
|
+
alert_eligible_roots=frozenset(), journal_emit=None, holder=None,
|
|
931
|
+
)
|
|
932
|
+
|
|
933
|
+
|
|
815
934
|
def reconcile_codex_quota_projection(
|
|
816
935
|
*,
|
|
817
936
|
source_root_keys: Iterable[str] | None = None,
|
|
@@ -875,82 +994,82 @@ def reconcile_codex_quota_projection(
|
|
|
875
994
|
finally:
|
|
876
995
|
cache.close()
|
|
877
996
|
|
|
878
|
-
#
|
|
879
|
-
# stats
|
|
880
|
-
stats
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
for block in blocks:
|
|
893
|
-
stats.execute(_BLOCK_UPSERT, _block_params(block, generation))
|
|
894
|
-
for milestone in percent_milestones(block):
|
|
895
|
-
stats.execute(
|
|
896
|
-
_MILESTONE_UPSERT,
|
|
897
|
-
_milestone_params(block, milestone, generation),
|
|
898
|
-
)
|
|
899
|
-
blocks_orphaned, milestones_orphaned = _orphan_unseen(
|
|
900
|
-
stats, roots_to_reconcile, generation, now_iso,
|
|
901
|
-
)
|
|
902
|
-
queued_alerts = _evaluate_quota_alerts(
|
|
903
|
-
stats, observations=observations,
|
|
904
|
-
alert_eligible_roots=alert_eligible_roots & active_roots,
|
|
905
|
-
now=now, now_iso=now_iso,
|
|
906
|
-
)
|
|
907
|
-
# The completion stamp is intentionally the final DML in the stats
|
|
908
|
-
# transaction. A pre-commit failure rolls all projection updates
|
|
909
|
-
# back; a retry sees the prior complete generation or rederives it.
|
|
910
|
-
for root_key in sorted(active_roots):
|
|
911
|
-
stats.execute(
|
|
912
|
-
"""INSERT INTO quota_projection_state
|
|
913
|
-
(source_root_key, generation, physical_signature, completed_at_utc)
|
|
914
|
-
VALUES (?,?,?,?)
|
|
915
|
-
ON CONFLICT(source_root_key) DO UPDATE SET
|
|
916
|
-
generation=excluded.generation,
|
|
917
|
-
physical_signature=excluded.physical_signature,
|
|
918
|
-
completed_at_utc=excluded.completed_at_utc""",
|
|
919
|
-
(root_key, generation, _signature(observations, root_key), now_iso),
|
|
920
|
-
)
|
|
921
|
-
if _before_stats_commit is not None:
|
|
922
|
-
_before_stats_commit()
|
|
923
|
-
stats.commit()
|
|
924
|
-
except Exception:
|
|
925
|
-
stats.rollback()
|
|
926
|
-
raise
|
|
927
|
-
finally:
|
|
928
|
-
stats.close()
|
|
997
|
+
# ── Apply phase (Task 7 Item 3) ─────────────────────────────────────────
|
|
998
|
+
# The stats.db writes route through the single-flight ingest cycle instead
|
|
999
|
+
# of this function opening its own stats connection + BEGIN IMMEDIATE. The
|
|
1000
|
+
# nested `_apply_projection(conn, sink)` is the transaction-neutral chokepoint
|
|
1001
|
+
# (writes on `conn`, no BEGIN/commit; alerts to `sink`) — the ingest cycle's
|
|
1002
|
+
# `codex_apply` seam drives it on the cycle's conn, AFTER the codex flock has
|
|
1003
|
+
# released (the existing after-flock-release rule). The interpreted `quota_*`
|
|
1004
|
+
# tables are mutable projections re-materialized here (spec §5.3), and each
|
|
1005
|
+
# genuine arming activation journals a `quota_alert_arming` evt so the
|
|
1006
|
+
# forward-only boundary survives a stats.db rebuild (Item 5).
|
|
1007
|
+
holder: dict = {
|
|
1008
|
+
"result": QuotaProjectionResult(None, 0, 0, 0, 0, 0, 0),
|
|
1009
|
+
"signatures": None,
|
|
1010
|
+
}
|
|
929
1011
|
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
1012
|
+
def _apply_projection(conn, sink, *, journal_emit=None):
|
|
1013
|
+
# No configured roots and no existing interpreted history means there is
|
|
1014
|
+
# no stats work. This preserves the existing empty-Codex fast path.
|
|
1015
|
+
# Delegates to the shared module-level apply so the live leg and the
|
|
1016
|
+
# rebuild re-materialization pass (Task 8) never drift.
|
|
1017
|
+
_apply_quota_projection_rows(
|
|
1018
|
+
conn, observations=observations, active_roots=active_roots,
|
|
1019
|
+
now=now, now_iso=now_iso, sink=sink,
|
|
1020
|
+
alert_eligible_roots=alert_eligible_roots,
|
|
1021
|
+
journal_emit=journal_emit, holder=holder,
|
|
1022
|
+
)
|
|
1023
|
+
|
|
1024
|
+
import _cctally_journal as _jr
|
|
1025
|
+
import _lib_journal as _jl
|
|
1026
|
+
|
|
1027
|
+
def _codex_leg(ctx):
|
|
1028
|
+
def _emit_arming(identity, fingerprint, activated_at):
|
|
1029
|
+
# Item 5: journal the arming state change (`quota_alert_arming` evt).
|
|
1030
|
+
eid = _jl.evt_id(
|
|
1031
|
+
"qaa", identity.source, identity.source_root_key,
|
|
1032
|
+
identity.logical_limit_key, identity.observed_slot,
|
|
1033
|
+
identity.window_minutes,
|
|
1034
|
+
)
|
|
1035
|
+
_jr.append_record(_jl.make_evt(
|
|
1036
|
+
kind="quota_alert_arming", id=eid, at=activated_at,
|
|
1037
|
+
payload={
|
|
1038
|
+
"source": identity.source,
|
|
1039
|
+
"source_root_key": identity.source_root_key,
|
|
1040
|
+
"logical_limit_key": identity.logical_limit_key,
|
|
1041
|
+
"observed_slot": identity.observed_slot,
|
|
1042
|
+
"window_minutes": identity.window_minutes,
|
|
1043
|
+
"rule_fingerprint": fingerprint,
|
|
1044
|
+
"activated_at_utc": activated_at,
|
|
1045
|
+
},
|
|
1046
|
+
))
|
|
1047
|
+
ctx.events_emitted += 1
|
|
1048
|
+
|
|
1049
|
+
_apply_projection(ctx.conn, ctx.pending_alerts, journal_emit=_emit_arming)
|
|
1050
|
+
# `_before_stats_commit` fires INSIDE the cycle txn, before COMMIT — a
|
|
1051
|
+
# raise rolls the whole cycle back, so the projection updates undo
|
|
1052
|
+
# together (the crash-consistency contract the callers test).
|
|
1053
|
+
if _before_stats_commit is not None:
|
|
1054
|
+
_before_stats_commit()
|
|
1055
|
+
|
|
1056
|
+
# AUTHORITATIVE: the reconcile must observe its own write + return the
|
|
1057
|
+
# materialized result synchronously; the cycle dispatches the queued alerts
|
|
1058
|
+
# post-commit via the ALERT_DISPATCHER, and `post_commit` runs the
|
|
1059
|
+
# `_after_stats_commit` seam after the commit (before dispatch).
|
|
1060
|
+
_jr.run_stats_ingest(
|
|
1061
|
+
mode="authoritative", codex_apply=_codex_leg,
|
|
1062
|
+
post_commit=_after_stats_commit,
|
|
952
1063
|
)
|
|
953
1064
|
|
|
1065
|
+
# Finalize: the cache-side certificate (self-healing no-op optimization) is
|
|
1066
|
+
# stored only when the apply actually materialized a projection.
|
|
1067
|
+
if holder["signatures"] is not None:
|
|
1068
|
+
_store_codex_quota_projection_certificate(
|
|
1069
|
+
sequence=physical_sequence, signatures=holder["signatures"],
|
|
1070
|
+
)
|
|
1071
|
+
return holder["result"]
|
|
1072
|
+
|
|
954
1073
|
|
|
955
1074
|
def _load_active_milestones(
|
|
956
1075
|
identity: QuotaWindowIdentity, resets_at: dt.datetime,
|