cctally 1.107.0 → 1.108.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 +48 -0
- package/bin/_cctally_alerts.py +147 -6
- package/bin/_cctally_core.py +298 -2
- package/bin/_cctally_dashboard.py +8 -20
- package/bin/_cctally_dashboard_envelope.py +103 -12
- package/bin/_cctally_diff.py +15 -8
- package/bin/_cctally_doctor.py +71 -3
- package/bin/_cctally_forecast.py +31 -6
- package/bin/_cctally_journal.py +280 -28
- package/bin/_cctally_parser.py +9 -5
- package/bin/_cctally_percent_breakdown.py +278 -12
- package/bin/_cctally_quota.py +16 -20
- package/bin/_cctally_quota_model.py +156 -15
- package/bin/_cctally_record.py +416 -217
- package/bin/_cctally_rederive.py +0 -2
- package/bin/_cctally_setup.py +272 -110
- package/bin/_cctally_statusline.py +30 -1
- package/bin/_cctally_tui.py +28 -25
- package/bin/_cctally_weekrefs.py +315 -102
- package/bin/_lib_alerts_payload.py +48 -1
- package/bin/_lib_codex_hooks.py +694 -83
- package/bin/_lib_diff_kernel.py +33 -10
- package/bin/_lib_doctor.py +172 -24
- package/bin/_lib_journal.py +64 -0
- package/bin/_lib_meter_rate_change.py +132 -3
- package/bin/_lib_pricing.py +22 -5
- package/bin/_lib_pricing_check.py +5 -4
- package/bin/_lib_subscription_weeks.py +140 -63
- package/bin/cctally +14 -3
- package/dashboard/static/assets/{ConversationsView-BHbw2W1l.js → ConversationsView-BOSaBRtu.js} +12 -12
- package/dashboard/static/assets/{DoctorModal-DcyMPwvn.js → DoctorModal-CWn3U4Wl.js} +1 -1
- package/dashboard/static/assets/ModalRoot-SR070V6c.js +1 -0
- package/dashboard/static/assets/{ProjectsDrillPanel-ecN8oCwv.js → ProjectsDrillPanel-QLI9i5mZ.js} +1 -1
- package/dashboard/static/assets/{SourceDetailModal-CUwdD7_v.js → SourceDetailModal-pv0wlFxl.js} +1 -1
- package/dashboard/static/assets/{UpdateModal-CKlFE0ER.js → UpdateModal-D3m8GG6V.js} +3 -3
- package/dashboard/static/assets/index-BHu4mxd8.css +1 -0
- package/dashboard/static/assets/index-CIWsbux3.js +13 -0
- package/dashboard/static/assets/{outlineNavigation-CVse0Hj9.js → outlineNavigation-CJvKqmLV.js} +1 -1
- package/dashboard/static/assets/useKeymap-ffqsS5G0.js +1 -0
- package/dashboard/static/dashboard.html +3 -3
- package/package.json +1 -1
- package/dashboard/static/assets/ModalRoot-BYV-99Rq.js +0 -1
- package/dashboard/static/assets/index-D8svRv_9.js +0 -13
- package/dashboard/static/assets/index-klO46NcU.css +0 -1
- package/dashboard/static/assets/useKeymap-CJ-Pi17D.js +0 -1
|
@@ -40,6 +40,7 @@ from __future__ import annotations
|
|
|
40
40
|
import bisect
|
|
41
41
|
import datetime as dt
|
|
42
42
|
import importlib.util as _ilu
|
|
43
|
+
import json
|
|
43
44
|
import os
|
|
44
45
|
import sqlite3
|
|
45
46
|
import sys
|
|
@@ -383,14 +384,23 @@ def _envelope_rows_weekly(
|
|
|
383
384
|
# collide on the duplicate (week, threshold) pair. Older clients
|
|
384
385
|
# tolerate longer ids — the id is opaque to them; only the React
|
|
385
386
|
# key uniqueness invariant matters.
|
|
387
|
+
# The LEFT JOIN resolves the segment number into the instant its cycle
|
|
388
|
+
# began (#750 S3). ``reset_event_id`` is a ``week_reset_events.id``, whose
|
|
389
|
+
# AUTOINCREMENT never issues 0, so the pre-credit sentinel matches no row
|
|
390
|
+
# and the mapper falls back to the week's own start below. The join is on
|
|
391
|
+
# the id alone: the id IS the identity the milestone recorded, and adding
|
|
392
|
+
# an account predicate could only turn a legitimately-stamped row into a
|
|
393
|
+
# silent NULL.
|
|
386
394
|
rows = conn.execute(
|
|
387
395
|
f"""
|
|
388
|
-
SELECT week_start_date, week_start_at, percent_threshold,
|
|
389
|
-
captured_at_utc, alerted_at, cumulative_cost_usd,
|
|
390
|
-
reset_event_id, account_key
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
396
|
+
SELECT m.week_start_date, m.week_start_at, m.percent_threshold,
|
|
397
|
+
m.captured_at_utc, m.alerted_at, m.cumulative_cost_usd,
|
|
398
|
+
m.reset_event_id, m.account_key,
|
|
399
|
+
e.effective_reset_at_utc
|
|
400
|
+
FROM {descriptor.milestone_table} m
|
|
401
|
+
LEFT JOIN week_reset_events e ON e.id = m.reset_event_id
|
|
402
|
+
WHERE m.alerted_at IS NOT NULL
|
|
403
|
+
ORDER BY {_CANON_ALERTED_AT_M} DESC
|
|
394
404
|
LIMIT ?
|
|
395
405
|
""",
|
|
396
406
|
(limit,),
|
|
@@ -420,6 +430,20 @@ def _envelope_rows_weekly(
|
|
|
420
430
|
# key stays on the wire and the reader degrades to day
|
|
421
431
|
# granularity instead of inventing a clock reading.
|
|
422
432
|
"week_start_at": r["week_start_at"] or "",
|
|
433
|
+
# The start instant of the BILLING CYCLE this crossing
|
|
434
|
+
# belongs to (#750 S3). A credit ends one cycle and begins
|
|
435
|
+
# another without moving either week boundary, so a week
|
|
436
|
+
# credited twice publishes three rows carrying one
|
|
437
|
+
# `week_start_date` and the week alone identifies none of
|
|
438
|
+
# them. Segment 0 has no reset event and its cycle begins
|
|
439
|
+
# with the week. Empty string when the row retains neither
|
|
440
|
+
# instant, mirroring `week_start_at` above. This is a
|
|
441
|
+
# SEPARATE field on purpose: `week_start_at` is what both
|
|
442
|
+
# scope kernels add seven days to, and the week's end does
|
|
443
|
+
# not move when a cycle inside it does.
|
|
444
|
+
"cycle_start_at": (
|
|
445
|
+
r["effective_reset_at_utc"] or r["week_start_at"] or ""
|
|
446
|
+
),
|
|
423
447
|
"cumulative_cost_usd": cumulative,
|
|
424
448
|
"dollars_per_percent": dpp,
|
|
425
449
|
# Round-3: parallel to the 5h context block below — both
|
|
@@ -850,22 +874,53 @@ def _build_meter_rate_change_array(
|
|
|
850
874
|
SCOPE (the account, decorated under R8 exactly as the alert rows are).
|
|
851
875
|
"""
|
|
852
876
|
account_fields = _alert_account_resolver(conn)
|
|
877
|
+
# #747: the stored `severity` column is unconstrained, so an older writer,
|
|
878
|
+
# a hand-repaired row or a future kernel can put a token here that no
|
|
879
|
+
# `RateChangeSeverity` border rule matches; such a row renders on the base
|
|
880
|
+
# amber border with nothing reporting it. Clamp to `info` through the
|
|
881
|
+
# kernel's own tuple — the same rule `_cctally_alerts._dispatch_alert_
|
|
882
|
+
# notification` already applies on the notifier path, applied at the second
|
|
883
|
+
# site rather than written out a third time.
|
|
884
|
+
#
|
|
885
|
+
# Clamped, not asserted: an assertion in the envelope builder would let one
|
|
886
|
+
# malformed stored row break the entire payload, which is a worse outcome
|
|
887
|
+
# than rendering that row at `info`.
|
|
888
|
+
#
|
|
889
|
+
# An unresolvable kernel degrades to NO clamp, which is the behaviour that
|
|
890
|
+
# shipped before this change. Degrading to an empty vocabulary instead
|
|
891
|
+
# would send every row — including every correct one — to `info`.
|
|
892
|
+
try:
|
|
893
|
+
_severities = frozenset(
|
|
894
|
+
sys.modules["cctally"]._load_sibling(
|
|
895
|
+
"_lib_meter_rate_change").RATE_CHANGE_SEVERITIES)
|
|
896
|
+
except Exception: # noqa: BLE001
|
|
897
|
+
_severities = None
|
|
853
898
|
try:
|
|
854
899
|
rows = conn.execute(
|
|
855
900
|
"SELECT provider, account_key, effective_from,"
|
|
856
901
|
" previous_units_per_point, new_units_per_point, severity,"
|
|
857
|
-
" detected_at_utc, created_at_utc"
|
|
902
|
+
" detected_at_utc, created_at_utc,"
|
|
903
|
+
" withholding_status, detector_input_causes,"
|
|
904
|
+
" composition_provenance, baseline_withheld_days"
|
|
858
905
|
" FROM meter_rate_change_events"
|
|
859
906
|
" ORDER BY unixepoch(effective_from) DESC, id DESC"
|
|
860
907
|
" LIMIT ?", (int(limit),)).fetchall()
|
|
861
908
|
except sqlite3.Error:
|
|
862
|
-
# A store predating epoch
|
|
863
|
-
#
|
|
864
|
-
#
|
|
909
|
+
# A store predating epoch 1012 does not have the shape this SELECT
|
|
910
|
+
# asks for: below 1011 there is no `meter_rate_change_events` table at
|
|
911
|
+
# all, and at 1011 the table exists but the four #690 disclosure
|
|
912
|
+
# columns do not. Both raise `sqlite3.Error` and both are answered the
|
|
913
|
+
# same way, because the rebuild that supplies either is deferred to a
|
|
914
|
+
# background worker. An empty array renders as "no change recorded",
|
|
915
|
+
# which is the truth on that store, rather than emptying the envelope.
|
|
865
916
|
return []
|
|
866
917
|
out: list[dict] = []
|
|
867
918
|
for (provider, account_key, effective_from, previous, new, severity,
|
|
868
|
-
detected_at, created_at
|
|
919
|
+
detected_at, created_at, status, causes, provenance,
|
|
920
|
+
baseline_days) in rows:
|
|
921
|
+
severity_token = str(severity or "info")
|
|
922
|
+
if _severities is not None and severity_token not in _severities:
|
|
923
|
+
severity_token = "info"
|
|
869
924
|
entry = {
|
|
870
925
|
# Opaque React key. It is never parsed — the same contract the
|
|
871
926
|
# alert `id` carries.
|
|
@@ -873,19 +928,55 @@ def _build_meter_rate_change_array(
|
|
|
873
928
|
"family": "meter_rate_change",
|
|
874
929
|
"provider": str(provider),
|
|
875
930
|
"owner": str(provider),
|
|
876
|
-
"severity":
|
|
931
|
+
"severity": severity_token,
|
|
877
932
|
"effective_from": str(effective_from),
|
|
878
933
|
"detected_at": str(detected_at or created_at or ""),
|
|
879
934
|
"recorded_at": str(created_at or ""),
|
|
880
935
|
"previous_units_per_point": (
|
|
881
936
|
None if previous is None else float(previous)),
|
|
882
937
|
"new_units_per_point": None if new is None else float(new),
|
|
938
|
+
# #690: the disclosure evidence, published ALWAYS and nulled
|
|
939
|
+
# rather than dropped. `null`, `[]` and `0` are three distinct
|
|
940
|
+
# states the client must be able to tell apart: null means legacy
|
|
941
|
+
# or unrecoverable evidence, `[]` means assessed with no such
|
|
942
|
+
# origin, and 0 means a clean baseline. The two array columns are
|
|
943
|
+
# decoded from their canonical JSON rather than forwarded as
|
|
944
|
+
# strings, so the client receives typed arrays; a column that
|
|
945
|
+
# will not decode degrades to null, which is the honest "cannot
|
|
946
|
+
# be read" answer and never an empty measurement.
|
|
947
|
+
"withholding_status": (
|
|
948
|
+
None if status is None else str(status)),
|
|
949
|
+
"detector_input_causes": _decode_evidence_array(causes),
|
|
950
|
+
"composition_provenance": _decode_evidence_array(provenance),
|
|
951
|
+
"baseline_withheld_days": (
|
|
952
|
+
None if baseline_days is None else int(baseline_days)),
|
|
883
953
|
}
|
|
884
954
|
entry.update(account_fields(str(provider), account_key))
|
|
885
955
|
out.append(entry)
|
|
886
956
|
return out
|
|
887
957
|
|
|
888
958
|
|
|
959
|
+
def _decode_evidence_array(value) -> "list | None":
|
|
960
|
+
"""One stored evidence column as a typed list, or None (#690).
|
|
961
|
+
|
|
962
|
+
The column holds a canonical JSON array of enum VALUES. Decoding here
|
|
963
|
+
rather than forwarding the string keeps the wire contract typed and keeps
|
|
964
|
+
the null-versus-empty distinction intact on the client: `null` and `[]`
|
|
965
|
+
are different answers and a string `"[]"` would be neither.
|
|
966
|
+
|
|
967
|
+
Anything that will not decode to a list degrades to None. That is the
|
|
968
|
+
honest "cannot be read" answer; returning `[]` would publish an empty
|
|
969
|
+
MEASUREMENT — assessed, no such origin — for a column nobody could read.
|
|
970
|
+
"""
|
|
971
|
+
if value is None:
|
|
972
|
+
return None
|
|
973
|
+
try:
|
|
974
|
+
decoded = json.loads(value)
|
|
975
|
+
except (TypeError, ValueError):
|
|
976
|
+
return None
|
|
977
|
+
return decoded if isinstance(decoded, list) else None
|
|
978
|
+
|
|
979
|
+
|
|
889
980
|
def _build_alerts_envelope_array(
|
|
890
981
|
conn: sqlite3.Connection,
|
|
891
982
|
limit: int = 100,
|
package/bin/_cctally_diff.py
CHANGED
|
@@ -118,11 +118,25 @@ def cmd_diff(args: argparse.Namespace) -> int:
|
|
|
118
118
|
# windows for Claude instead of attempting to reinterpret the original
|
|
119
119
|
# tokens against its subscription-week anchor. Ordinary Claude invocations
|
|
120
120
|
# keep the established anchor/parser path byte-for-byte.
|
|
121
|
+
# #341 --account: resolve the render filter (provider=claude; fail closed
|
|
122
|
+
# with exit 3 when the entry cache is unavailable). None = merged.
|
|
123
|
+
#
|
|
124
|
+
# #750 S3 B3: this resolves BEFORE the anchor, not a hundred lines after
|
|
125
|
+
# both windows are built. `docs/accounts-gotchas.md` states the rule — the
|
|
126
|
+
# account resolves before the interval is constructed — because an
|
|
127
|
+
# account-filtered read whose interval came from the merged boundary set
|
|
128
|
+
# buckets one account's dollars into a window that belongs to neither.
|
|
129
|
+
acct_key, acct_exit = c.resolve_account_filter(
|
|
130
|
+
args, "claude", needs_cache=True)
|
|
131
|
+
if acct_exit is not None:
|
|
132
|
+
return acct_exit
|
|
133
|
+
|
|
121
134
|
supplied_windows = getattr(args, "_source_analytics_windows", None)
|
|
122
135
|
if supplied_windows is None:
|
|
123
136
|
# Resolve anchors (None when no snapshots exist; week tokens then
|
|
124
137
|
# raise NoAnchorError in the parser).
|
|
125
|
-
anchor_week_start, anchor_resets_at = dk._diff_resolve_anchor(
|
|
138
|
+
anchor_week_start, anchor_resets_at = dk._diff_resolve_anchor(
|
|
139
|
+
now_utc, account_key=acct_key)
|
|
126
140
|
else:
|
|
127
141
|
anchor_week_start, anchor_resets_at = None, None
|
|
128
142
|
|
|
@@ -222,13 +236,6 @@ def cmd_diff(args: argparse.Namespace) -> int:
|
|
|
222
236
|
if args.min_delta_pct is not None:
|
|
223
237
|
threshold = dataclasses.replace(threshold, min_delta_pct=args.min_delta_pct)
|
|
224
238
|
|
|
225
|
-
# #341 --account: resolve the render filter (provider=claude; fail closed
|
|
226
|
-
# with exit 3 when the entry cache is unavailable). None = merged.
|
|
227
|
-
acct_key, acct_exit = c.resolve_account_filter(
|
|
228
|
-
args, "claude", needs_cache=True)
|
|
229
|
-
if acct_exit is not None:
|
|
230
|
-
return acct_exit
|
|
231
|
-
|
|
232
239
|
try:
|
|
233
240
|
result = dk._build_diff_result(
|
|
234
241
|
window_a, window_b,
|
package/bin/_cctally_doctor.py
CHANGED
|
@@ -31,6 +31,7 @@ import json
|
|
|
31
31
|
import math
|
|
32
32
|
import os
|
|
33
33
|
import pathlib
|
|
34
|
+
import re
|
|
34
35
|
import shutil
|
|
35
36
|
import sqlite3
|
|
36
37
|
import subprocess
|
|
@@ -588,6 +589,61 @@ def _codex_lifecycle_activity_24h(
|
|
|
588
589
|
return records
|
|
589
590
|
|
|
590
591
|
|
|
592
|
+
_CODEX_ACCOUNT_KEY_PATTERN = re.compile(r"^[0-9a-f]{32}$")
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def _codex_hook_liveness_markers(*, root_keys: set[str]) -> dict[str, dict]:
|
|
596
|
+
"""Enumerate `<root>.last-success` / `<root>.<account>.last-success` mtimes.
|
|
597
|
+
|
|
598
|
+
Marker enumeration lives here, never in `bin/_lib_doctor.py`: the pure
|
|
599
|
+
kernel decides every check without touching the filesystem, the network
|
|
600
|
+
or the clock, and this check is not the exception.
|
|
601
|
+
|
|
602
|
+
A bare `*` glob would also admit backup-like siblings, so the account
|
|
603
|
+
suffix is filtered against the 32-hex `account_key` shape rather than
|
|
604
|
+
globbed. Account suffixes are never exposed to the report.
|
|
605
|
+
"""
|
|
606
|
+
if not root_keys:
|
|
607
|
+
return {}
|
|
608
|
+
base = _cctally_core.APP_DIR / "codex-hook-tick"
|
|
609
|
+
records: dict[str, dict] = {
|
|
610
|
+
key: {"last_success_at": None, "marker_count": 0, "unavailable": False}
|
|
611
|
+
for key in root_keys
|
|
612
|
+
}
|
|
613
|
+
try:
|
|
614
|
+
names = sorted(entry.name for entry in os.scandir(base))
|
|
615
|
+
except FileNotFoundError:
|
|
616
|
+
# No marker directory yet is `never`, not `unavailable`: the hook has
|
|
617
|
+
# simply not run here.
|
|
618
|
+
return records
|
|
619
|
+
except OSError:
|
|
620
|
+
for record in records.values():
|
|
621
|
+
record["unavailable"] = True
|
|
622
|
+
return records
|
|
623
|
+
for name in names:
|
|
624
|
+
if not name.endswith(".last-success"):
|
|
625
|
+
continue
|
|
626
|
+
stem = name[: -len(".last-success")]
|
|
627
|
+
root_key = stem
|
|
628
|
+
if stem not in records:
|
|
629
|
+
head, _sep, suffix = stem.rpartition(".")
|
|
630
|
+
if not head or not _CODEX_ACCOUNT_KEY_PATTERN.match(suffix):
|
|
631
|
+
continue
|
|
632
|
+
root_key = head
|
|
633
|
+
if root_key not in records:
|
|
634
|
+
continue
|
|
635
|
+
try:
|
|
636
|
+
stamp = (base / name).stat().st_mtime
|
|
637
|
+
except OSError:
|
|
638
|
+
continue
|
|
639
|
+
record = records[root_key]
|
|
640
|
+
record["marker_count"] += 1
|
|
641
|
+
observed = dt.datetime.fromtimestamp(stamp, dt.timezone.utc)
|
|
642
|
+
if record["last_success_at"] is None or observed > record["last_success_at"]:
|
|
643
|
+
record["last_success_at"] = observed
|
|
644
|
+
return records
|
|
645
|
+
|
|
646
|
+
|
|
591
647
|
def _codex_quota_verify_activity_24h(*, now_utc: "dt.datetime") -> dict:
|
|
592
648
|
"""Aggregate the detached `_codex-quota-verify` worker's 24h outcomes.
|
|
593
649
|
|
|
@@ -1801,18 +1857,29 @@ def _doctor_gather_state_impl(
|
|
|
1801
1857
|
|
|
1802
1858
|
with _lib_perf.phase("doctor.codex_hooks"):
|
|
1803
1859
|
codex_hook_roots: list[dict] = []
|
|
1860
|
+
codex_hook_liveness: dict = {}
|
|
1804
1861
|
try:
|
|
1805
|
-
codex_binary = str(c._setup_resolve_hook_target(repo_root))
|
|
1806
1862
|
hook_rows = [
|
|
1807
|
-
c._cctally_setup._codex_hook_row(root
|
|
1863
|
+
c._cctally_setup._codex_hook_row(root)
|
|
1808
1864
|
for root in c._setup_codex_hook_roots()
|
|
1809
1865
|
]
|
|
1810
1866
|
codex_hook_roots = [
|
|
1811
|
-
{
|
|
1867
|
+
{
|
|
1868
|
+
"source_root_key": row["source_root_key"],
|
|
1869
|
+
"state": row["state"],
|
|
1870
|
+
"requires_review": row["requires_review"],
|
|
1871
|
+
"remediation": row["remediation"],
|
|
1872
|
+
}
|
|
1812
1873
|
for row in sorted(hook_rows, key=lambda row: row["source_root_key"])
|
|
1813
1874
|
]
|
|
1814
1875
|
except Exception:
|
|
1815
1876
|
codex_hook_roots = []
|
|
1877
|
+
try:
|
|
1878
|
+
codex_hook_liveness = _codex_hook_liveness_markers(
|
|
1879
|
+
root_keys={row["source_root_key"] for row in codex_hook_roots},
|
|
1880
|
+
)
|
|
1881
|
+
except Exception:
|
|
1882
|
+
codex_hook_liveness = {}
|
|
1816
1883
|
|
|
1817
1884
|
with _lib_perf.phase("doctor.codex_lifecycle"):
|
|
1818
1885
|
try:
|
|
@@ -2601,6 +2668,7 @@ def _doctor_gather_state_impl(
|
|
|
2601
2668
|
conversations_db_freelist_count=conversations_db_freelist_count,
|
|
2602
2669
|
codex_quota_windows=codex_quota_windows,
|
|
2603
2670
|
codex_hook_roots=codex_hook_roots,
|
|
2671
|
+
codex_hook_liveness=codex_hook_liveness,
|
|
2604
2672
|
codex_lifecycle_activity_24h=codex_lifecycle_activity_24h,
|
|
2605
2673
|
codex_quota_verify_activity=codex_quota_verify_activity,
|
|
2606
2674
|
# #311: precomputed statusLine.refreshInterval classification.
|
package/bin/_cctally_forecast.py
CHANGED
|
@@ -2134,13 +2134,22 @@ def cmd_report(args: argparse.Namespace) -> int:
|
|
|
2134
2134
|
# effective reset moment). Route `current_ref` through the same
|
|
2135
2135
|
# override so its `week_start_at` reflects the post-credit start;
|
|
2136
2136
|
# this lets the per-row match below disambiguate the synthesized
|
|
2137
|
-
#
|
|
2138
|
-
#
|
|
2139
|
-
#
|
|
2140
|
-
#
|
|
2137
|
+
# earlier refs from the live one via both `key` AND `week_start_at`.
|
|
2138
|
+
# Order contract from `_apply_reset_events_to_weekrefs`: the output is
|
|
2139
|
+
# NEWEST-FIRST, so the live segment is at index 0 and every earlier
|
|
2140
|
+
# segment follows it in descending order. #750 S3 made the applier
|
|
2141
|
+
# N-ary, so index 1 is the segment before the live one rather than
|
|
2142
|
+
# "the pre-credit ref" — a week credited twice puts a middle segment
|
|
2143
|
+
# there. Only index 0 is read here. Non-credit weeks return the
|
|
2141
2144
|
# single input ref unchanged, so this is a no-op on the common
|
|
2142
2145
|
# path.
|
|
2143
|
-
|
|
2146
|
+
# SCOPED to the requesting account (#750 S3, Unit B review). The match
|
|
2147
|
+
# below disambiguates the live segment by `week_start_at`, so a merged
|
|
2148
|
+
# read lets ANOTHER account's cut shift that value and the current-week
|
|
2149
|
+
# row is then misidentified. `acct_key` is None without `--account`,
|
|
2150
|
+
# which is the explicit merged read this call always made.
|
|
2151
|
+
_adjusted_current = c._apply_reset_events_to_weekrefs(
|
|
2152
|
+
conn, [current_ref], account_key=acct_key)
|
|
2144
2153
|
if _adjusted_current:
|
|
2145
2154
|
current_ref = _adjusted_current[0]
|
|
2146
2155
|
|
|
@@ -2396,12 +2405,28 @@ def cmd_report(args: argparse.Namespace) -> int:
|
|
|
2396
2405
|
if milestone_rows:
|
|
2397
2406
|
print()
|
|
2398
2407
|
print("Percent breakdown (current week):\n")
|
|
2408
|
+
# #750 S3 Unit C (issue #738). This table is a SECOND surface
|
|
2409
|
+
# over the same rows, with its own column set and its own bare
|
|
2410
|
+
# `n/a`, so it had the same defect and shipping the disclosure
|
|
2411
|
+
# on only one of the two would leave the issue half fixed. The
|
|
2412
|
+
# classifier is C1's, reached on the namespace — the predicate
|
|
2413
|
+
# has one home. It groups by `(account_key, reset_event_id)`
|
|
2414
|
+
# itself, which matters here because this table is NOT
|
|
2415
|
+
# segment-filtered and renders every epoch of the week.
|
|
2416
|
+
gap_disclosure = c.classify_observation_gaps(milestone_rows)
|
|
2417
|
+
for note in c.observation_gap_notes(gap_disclosure, tz=tz):
|
|
2418
|
+
print(note)
|
|
2419
|
+
if gap_disclosure.runs:
|
|
2420
|
+
print()
|
|
2399
2421
|
m_headers = ["#", "Threshold", "Cumulative Cost", "Marginal Cost"]
|
|
2400
2422
|
m_rows: list[list[str]] = []
|
|
2401
2423
|
for idx, m in enumerate(milestone_rows, start=1):
|
|
2402
2424
|
pct = f"{int(m['percent_threshold'])}%"
|
|
2403
2425
|
cum = f"${float(m['cumulative_cost_usd']):.6f}"
|
|
2404
|
-
marg =
|
|
2426
|
+
marg = c.observation_gap_marginal_cell(
|
|
2427
|
+
m["marginal_cost_usd"],
|
|
2428
|
+
withheld=(idx - 1) in gap_disclosure.withheld_indexes,
|
|
2429
|
+
)
|
|
2405
2430
|
m_rows.append([str(idx), pct, cum, marg])
|
|
2406
2431
|
print(c._boxed_table(m_headers, m_rows, ["right", "right", "right", "right"]))
|
|
2407
2432
|
|