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
|
@@ -22,6 +22,7 @@ import sys
|
|
|
22
22
|
from dataclasses import dataclass
|
|
23
23
|
|
|
24
24
|
from _cctally_core import (
|
|
25
|
+
_as_of_or_command,
|
|
25
26
|
_budget_alerts_active,
|
|
26
27
|
_canonicalize_optional_iso,
|
|
27
28
|
_command_as_of,
|
|
@@ -126,11 +127,56 @@ def insert_cost_snapshot(
|
|
|
126
127
|
cost_usd: float,
|
|
127
128
|
mode: str,
|
|
128
129
|
project: str | None,
|
|
130
|
+
*,
|
|
131
|
+
commit: bool = True,
|
|
132
|
+
as_of: "str | None" = None,
|
|
133
|
+
journal: "tuple | None" = None,
|
|
129
134
|
) -> int:
|
|
135
|
+
"""Insert a ``weekly_cost_snapshots`` row and return its rowid.
|
|
136
|
+
|
|
137
|
+
Transaction-neutral / capture-time-pure seam (DB journal redesign §5.2.3):
|
|
138
|
+
``commit=False`` skips the inner ``conn.commit()`` so the ingester can bundle
|
|
139
|
+
this insert into its single cycle transaction; ``as_of`` (ISO-Z) overrides
|
|
140
|
+
the ``captured_at_utc`` wall-clock stamp with the record's capture time.
|
|
141
|
+
Both defaults keep legacy callers bit-identical.
|
|
142
|
+
|
|
143
|
+
Design A (DB journal redesign §5.3, Model-A ``weekly_cost_snapshot``):
|
|
144
|
+
``journal=(ctx, id_base)`` routes the insert THROUGH ``emit_model_a`` — the
|
|
145
|
+
computed cost rides in the journaled ``columns`` so replay reads it back
|
|
146
|
+
verbatim and NEVER recomputes from provider JSONL (which Claude Code prunes).
|
|
147
|
+
``id_base`` is the triggering record's logical id (the obs line id on the
|
|
148
|
+
record-usage path, the op line id on the sync-week op path); the evt id is
|
|
149
|
+
``wcs:<id_base>:<week_start_date>``. Default ``None`` is today's bare insert
|
|
150
|
+
and must never append an evt. Returns the target rowid (fresh or converged
|
|
151
|
+
crash-replay) exactly as the direct path returns ``lastrowid``.
|
|
152
|
+
"""
|
|
130
153
|
start_at = _canonicalize_optional_iso(week_start_at, "weekStartAt")
|
|
131
154
|
end_at = _canonicalize_optional_iso(week_end_at, "weekEndAt")
|
|
132
155
|
range_start = parse_iso_datetime(range_start_iso, "rangeStartIso").isoformat(timespec="seconds")
|
|
133
156
|
range_end = parse_iso_datetime(range_end_iso, "rangeEndIso").isoformat(timespec="seconds")
|
|
157
|
+
captured = as_of or now_utc_iso()
|
|
158
|
+
if journal is not None:
|
|
159
|
+
ctx, id_base = journal
|
|
160
|
+
import _cctally_journal
|
|
161
|
+
return _cctally_journal.emit_model_a(
|
|
162
|
+
ctx,
|
|
163
|
+
kind="weekly_cost_snapshot",
|
|
164
|
+
evt_id=f"wcs:{id_base}:{week_start.isoformat()}",
|
|
165
|
+
table="weekly_cost_snapshots",
|
|
166
|
+
columns={
|
|
167
|
+
"captured_at_utc": captured,
|
|
168
|
+
"week_start_date": week_start.isoformat(),
|
|
169
|
+
"week_end_date": week_end.isoformat(),
|
|
170
|
+
"week_start_at": start_at,
|
|
171
|
+
"week_end_at": end_at,
|
|
172
|
+
"range_start_iso": range_start,
|
|
173
|
+
"range_end_iso": range_end,
|
|
174
|
+
"cost_usd": cost_usd,
|
|
175
|
+
"mode": mode,
|
|
176
|
+
"project": project,
|
|
177
|
+
},
|
|
178
|
+
at=captured,
|
|
179
|
+
)
|
|
134
180
|
cur = conn.execute(
|
|
135
181
|
"""
|
|
136
182
|
INSERT INTO weekly_cost_snapshots
|
|
@@ -149,7 +195,7 @@ def insert_cost_snapshot(
|
|
|
149
195
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
150
196
|
""",
|
|
151
197
|
(
|
|
152
|
-
|
|
198
|
+
captured,
|
|
153
199
|
week_start.isoformat(),
|
|
154
200
|
week_end.isoformat(),
|
|
155
201
|
start_at,
|
|
@@ -161,7 +207,8 @@ def insert_cost_snapshot(
|
|
|
161
207
|
project,
|
|
162
208
|
),
|
|
163
209
|
)
|
|
164
|
-
|
|
210
|
+
if commit:
|
|
211
|
+
conn.commit()
|
|
165
212
|
return int(cur.lastrowid)
|
|
166
213
|
|
|
167
214
|
|
|
@@ -257,9 +304,14 @@ def insert_percent_milestone(
|
|
|
257
304
|
*,
|
|
258
305
|
commit: bool = True,
|
|
259
306
|
reset_event_id: int = 0,
|
|
307
|
+
as_of: "str | None" = None,
|
|
260
308
|
) -> int:
|
|
261
309
|
"""Insert a percent_milestones row idempotently.
|
|
262
310
|
|
|
311
|
+
``as_of`` (DB journal redesign §5.2.3): overrides the ``captured_at_utc``
|
|
312
|
+
wall-clock stamp with the record's capture time when the ingester drives
|
|
313
|
+
this at fold time; ``None`` keeps the legacy ``now_utc_iso()`` behavior.
|
|
314
|
+
|
|
263
315
|
Returns the SQLite rowcount: 1 on a genuinely new crossing, 0 if a row
|
|
264
316
|
for (week_start_date, percent_threshold, reset_event_id) already exists.
|
|
265
317
|
Race-safe under concurrent record-usage instances — aligns with the
|
|
@@ -309,7 +361,7 @@ def insert_percent_milestone(
|
|
|
309
361
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
310
362
|
""",
|
|
311
363
|
(
|
|
312
|
-
now_utc_iso(),
|
|
364
|
+
as_of or now_utc_iso(),
|
|
313
365
|
week_start_date,
|
|
314
366
|
week_end_date,
|
|
315
367
|
week_start_at,
|
|
@@ -339,6 +391,7 @@ def insert_budget_milestone(
|
|
|
339
391
|
spent_usd: float,
|
|
340
392
|
consumption_pct: float,
|
|
341
393
|
commit: bool = True,
|
|
394
|
+
as_of: "str | None" = None,
|
|
342
395
|
) -> int:
|
|
343
396
|
"""INSERT OR IGNORE a budget threshold crossing into the unified vendor-tagged
|
|
344
397
|
table (#143). Returns ``cur.rowcount`` (1 = genuinely new crossing, 0 =
|
|
@@ -374,7 +427,7 @@ def insert_budget_milestone(
|
|
|
374
427
|
float(budget_usd),
|
|
375
428
|
float(spent_usd),
|
|
376
429
|
float(consumption_pct),
|
|
377
|
-
now_utc_iso(),
|
|
430
|
+
as_of or now_utc_iso(),
|
|
378
431
|
),
|
|
379
432
|
)
|
|
380
433
|
if commit:
|
|
@@ -392,6 +445,7 @@ def insert_project_budget_milestone(
|
|
|
392
445
|
spent_usd: float,
|
|
393
446
|
consumption_pct: float,
|
|
394
447
|
commit: bool = True,
|
|
448
|
+
as_of: "str | None" = None,
|
|
395
449
|
) -> int:
|
|
396
450
|
"""INSERT OR IGNORE a per-project budget threshold crossing. Returns
|
|
397
451
|
``cur.rowcount`` (1 = genuinely new crossing, 0 = INSERT OR IGNORE no-op on a
|
|
@@ -420,7 +474,7 @@ def insert_project_budget_milestone(
|
|
|
420
474
|
float(budget_usd),
|
|
421
475
|
float(spent_usd),
|
|
422
476
|
float(consumption_pct),
|
|
423
|
-
now_utc_iso(),
|
|
477
|
+
as_of or now_utc_iso(),
|
|
424
478
|
),
|
|
425
479
|
)
|
|
426
480
|
if commit:
|
|
@@ -438,6 +492,7 @@ def insert_projected_milestone(
|
|
|
438
492
|
projected_value: float,
|
|
439
493
|
denominator: float,
|
|
440
494
|
commit: bool = True,
|
|
495
|
+
as_of: "str | None" = None,
|
|
441
496
|
) -> int:
|
|
442
497
|
"""INSERT OR IGNORE a projected-pace crossing. Returns ``cur.rowcount``
|
|
443
498
|
(1 = genuinely new crossing, 0 = INSERT OR IGNORE no-op on a pre-existing
|
|
@@ -466,7 +521,7 @@ def insert_projected_milestone(
|
|
|
466
521
|
int(threshold),
|
|
467
522
|
float(projected_value),
|
|
468
523
|
float(denominator),
|
|
469
|
-
now_utc_iso(),
|
|
524
|
+
as_of or now_utc_iso(),
|
|
470
525
|
),
|
|
471
526
|
)
|
|
472
527
|
if commit:
|
|
@@ -562,6 +617,7 @@ def _budget_spend_for_vendor(conn, *, vendor, start_at, now_utc) -> float:
|
|
|
562
617
|
|
|
563
618
|
def _reconcile_budget_milestones_on_set(
|
|
564
619
|
conn, *, vendor, target, thresholds, now_utc, period, config=None, tz=None,
|
|
620
|
+
as_of=None, commit=True,
|
|
565
621
|
):
|
|
566
622
|
"""Forward-only-from-set reconcile for the budget axis (both vendors, #143):
|
|
567
623
|
on `budget set`, every threshold ALREADY crossed for the current
|
|
@@ -605,6 +661,7 @@ def _reconcile_budget_milestones_on_set(
|
|
|
605
661
|
spent_usd=spent,
|
|
606
662
|
consumption_pct=consumption_pct,
|
|
607
663
|
commit=False,
|
|
664
|
+
as_of=as_of,
|
|
608
665
|
)
|
|
609
666
|
# alerted_at UPDATE keys on the CONCRETE (vendor, period) (not the
|
|
610
667
|
# wildcard): only the row we just inserted is stamped, never a
|
|
@@ -613,9 +670,10 @@ def _reconcile_budget_milestones_on_set(
|
|
|
613
670
|
"UPDATE budget_milestones SET alerted_at = ? "
|
|
614
671
|
"WHERE vendor = ? AND period_start_at = ? AND period = ? "
|
|
615
672
|
" AND threshold = ? AND alerted_at IS NULL",
|
|
616
|
-
(now_utc_iso(), vendor, period_key, period, t),
|
|
673
|
+
(as_of or now_utc_iso(), vendor, period_key, period, t),
|
|
617
674
|
)
|
|
618
|
-
|
|
675
|
+
if commit:
|
|
676
|
+
conn.commit()
|
|
619
677
|
|
|
620
678
|
|
|
621
679
|
def _resolve_codex_budget_period_window(period, now_utc, config, tz):
|
|
@@ -631,7 +689,8 @@ def _resolve_codex_budget_period_window(period, now_utc, config, tz):
|
|
|
631
689
|
|
|
632
690
|
|
|
633
691
|
def _budget_crossings(
|
|
634
|
-
conn, *, vendor, period_key, period=None, thresholds, target, spent, now_utc
|
|
692
|
+
conn, *, vendor, period_key, period=None, thresholds, target, spent, now_utc,
|
|
693
|
+
as_of=None,
|
|
635
694
|
):
|
|
636
695
|
"""Shared INSERT-and-arm core for the budget axis (both vendors, #143): for
|
|
637
696
|
every STILL-pending threshold that's been crossed at ``spent``, ``INSERT OR
|
|
@@ -667,9 +726,10 @@ def _budget_crossings(
|
|
|
667
726
|
spent_usd=spent,
|
|
668
727
|
consumption_pct=consumption_pct,
|
|
669
728
|
commit=False,
|
|
729
|
+
as_of=as_of,
|
|
670
730
|
)
|
|
671
731
|
if inserted == 1:
|
|
672
|
-
crossed_at = now_utc_iso()
|
|
732
|
+
crossed_at = as_of or now_utc_iso()
|
|
673
733
|
# alerted_at UPDATE keys on the CONCRETE (vendor, period) (#137 /
|
|
674
734
|
# #143): only the row just inserted is stamped, never a pre-011
|
|
675
735
|
# NULL-period sibling or another vendor's row.
|
|
@@ -683,13 +743,18 @@ def _budget_crossings(
|
|
|
683
743
|
return fired
|
|
684
744
|
|
|
685
745
|
|
|
686
|
-
def _reconcile_codex_budget_on_config_write(validated_budget):
|
|
746
|
+
def _reconcile_codex_budget_on_config_write(validated_budget, *, conn=None, as_of=None):
|
|
687
747
|
"""Forward-only reconcile shared by the Codex-budget config write paths
|
|
688
748
|
(`budget set --vendor codex`, `config set budget.codex`). Gated +
|
|
689
749
|
best-effort: a Codex budget with alerts off or no thresholds records
|
|
690
750
|
nothing; a stats.db failure never fails the write. Runs OUTSIDE any
|
|
691
751
|
config_writer_lock (open_db has its own locking). Mirrors
|
|
692
|
-
:func:`_reconcile_budget_on_config_write`.
|
|
752
|
+
:func:`_reconcile_budget_on_config_write`.
|
|
753
|
+
|
|
754
|
+
Transaction-neutral / capture-time-pure seam (DB journal redesign §5.2.3):
|
|
755
|
+
``conn`` runs the reconcile on the caller's connection (no internal
|
|
756
|
+
open/commit/close); ``as_of`` (ISO-Z) injects the capture time. Both
|
|
757
|
+
defaults keep the legacy own-connection, wall-clock behavior."""
|
|
693
758
|
codex = (validated_budget or {}).get("codex")
|
|
694
759
|
if not codex:
|
|
695
760
|
return
|
|
@@ -697,57 +762,72 @@ def _reconcile_codex_budget_on_config_write(validated_budget):
|
|
|
697
762
|
if not (codex.get("alerts_enabled") and codex.get("amount_usd") and thresholds):
|
|
698
763
|
return
|
|
699
764
|
c = _cctally()
|
|
765
|
+
own_conn = conn is None
|
|
700
766
|
try:
|
|
701
767
|
import argparse
|
|
702
768
|
config = c.load_config()
|
|
703
769
|
tz = c.resolve_display_tz(argparse.Namespace(tz=None), config)
|
|
704
|
-
|
|
770
|
+
if own_conn:
|
|
771
|
+
conn = open_db()
|
|
705
772
|
try:
|
|
706
773
|
_reconcile_budget_milestones_on_set(
|
|
707
774
|
conn,
|
|
708
775
|
vendor="codex",
|
|
709
776
|
target=codex["amount_usd"],
|
|
710
777
|
thresholds=thresholds,
|
|
711
|
-
now_utc=
|
|
778
|
+
now_utc=_as_of_or_command(as_of),
|
|
712
779
|
period=codex["period"],
|
|
713
780
|
config=config,
|
|
714
781
|
tz=tz,
|
|
782
|
+
as_of=as_of,
|
|
783
|
+
commit=own_conn,
|
|
715
784
|
)
|
|
716
785
|
finally:
|
|
717
|
-
|
|
786
|
+
if own_conn:
|
|
787
|
+
conn.close()
|
|
718
788
|
except Exception as exc: # best-effort; never fail the write
|
|
719
789
|
eprint(f"[codex-budget-milestone] reconcile on set failed: {exc}")
|
|
720
790
|
|
|
721
791
|
|
|
722
|
-
def _reconcile_budget_on_config_write(validated_budget):
|
|
792
|
+
def _reconcile_budget_on_config_write(validated_budget, *, conn=None, as_of=None):
|
|
723
793
|
"""Forward-only reconcile shared by all three budget-config write
|
|
724
794
|
paths (`budget set`, `config set budget.*`, dashboard POST
|
|
725
795
|
/api/settings). Gated + best-effort: a budget with alerts off or no
|
|
726
796
|
thresholds records nothing; a stats.db failure never fails the write.
|
|
727
|
-
Runs OUTSIDE any config_writer_lock (open_db has its own locking).
|
|
797
|
+
Runs OUTSIDE any config_writer_lock (open_db has its own locking).
|
|
798
|
+
|
|
799
|
+
Transaction-neutral / capture-time-pure seam (DB journal redesign §5.2.3):
|
|
800
|
+
``conn`` runs the reconcile on the caller's connection (no internal
|
|
801
|
+
open/commit/close); ``as_of`` (ISO-Z) injects the capture time. Both
|
|
802
|
+
defaults keep the legacy own-connection, wall-clock behavior."""
|
|
728
803
|
thresholds = validated_budget.get("alert_thresholds") or []
|
|
729
804
|
if not (_budget_alerts_active(validated_budget) and thresholds):
|
|
730
805
|
return
|
|
731
806
|
c = _cctally()
|
|
732
807
|
period = validated_budget.get("period", "subscription-week")
|
|
808
|
+
own_conn = conn is None
|
|
733
809
|
try:
|
|
734
810
|
import argparse
|
|
735
811
|
config = c.load_config()
|
|
736
812
|
tz = c.resolve_display_tz(argparse.Namespace(tz=None), config)
|
|
737
|
-
|
|
813
|
+
if own_conn:
|
|
814
|
+
conn = open_db()
|
|
738
815
|
try:
|
|
739
816
|
_reconcile_budget_milestones_on_set(
|
|
740
817
|
conn,
|
|
741
818
|
vendor="claude",
|
|
742
819
|
target=validated_budget["weekly_usd"],
|
|
743
820
|
thresholds=thresholds,
|
|
744
|
-
now_utc=
|
|
821
|
+
now_utc=_as_of_or_command(as_of),
|
|
745
822
|
period=period,
|
|
746
823
|
config=config,
|
|
747
824
|
tz=tz,
|
|
825
|
+
as_of=as_of,
|
|
826
|
+
commit=own_conn,
|
|
748
827
|
)
|
|
749
828
|
finally:
|
|
750
|
-
|
|
829
|
+
if own_conn:
|
|
830
|
+
conn.close()
|
|
751
831
|
except Exception as exc: # best-effort; never fail the write
|
|
752
832
|
eprint(f"[budget-milestone] reconcile on set failed: {exc}")
|
|
753
833
|
|
|
@@ -770,10 +850,15 @@ def _project_crossings(items, thresholds, by_proj):
|
|
|
770
850
|
|
|
771
851
|
|
|
772
852
|
def _reconcile_project_budget_milestones_on_write(
|
|
773
|
-
validated_budget, touched_projects=None
|
|
853
|
+
validated_budget, touched_projects=None, *, conn=None, as_of=None
|
|
774
854
|
):
|
|
775
855
|
"""Forward-only-from-write reconcile for PER-PROJECT budgets (spec §6.8).
|
|
776
856
|
|
|
857
|
+
Transaction-neutral / capture-time-pure seam (DB journal redesign §5.2.3):
|
|
858
|
+
``conn`` runs the reconcile on the caller's connection (no internal
|
|
859
|
+
open/commit/close); ``as_of`` (ISO-Z) injects the capture time. Both
|
|
860
|
+
defaults keep the legacy own-connection, wall-clock behavior.
|
|
861
|
+
|
|
777
862
|
Shared by all four per-project write surfaces: ``budget set --project`` /
|
|
778
863
|
``unset --project`` (call sites in ``_cmd_budget_set_project`` /
|
|
779
864
|
``_cmd_budget_unset_project``), ``config set budget.projects`` /
|
|
@@ -818,10 +903,12 @@ def _reconcile_project_budget_milestones_on_write(
|
|
|
818
903
|
):
|
|
819
904
|
return
|
|
820
905
|
c = _cctally()
|
|
906
|
+
own_conn = conn is None
|
|
821
907
|
try:
|
|
822
|
-
|
|
908
|
+
if own_conn:
|
|
909
|
+
conn = open_db()
|
|
823
910
|
try:
|
|
824
|
-
now_utc =
|
|
911
|
+
now_utc = _as_of_or_command(as_of)
|
|
825
912
|
window = c._resolve_current_budget_window(conn, now_utc)
|
|
826
913
|
if window is None:
|
|
827
914
|
return
|
|
@@ -852,16 +939,19 @@ def _reconcile_project_budget_milestones_on_write(
|
|
|
852
939
|
spent_usd=spent,
|
|
853
940
|
consumption_pct=consumption_pct,
|
|
854
941
|
commit=False,
|
|
942
|
+
as_of=as_of,
|
|
855
943
|
)
|
|
856
944
|
conn.execute(
|
|
857
945
|
"UPDATE project_budget_milestones SET alerted_at = ? "
|
|
858
946
|
"WHERE week_start_at = ? AND project_key = ? "
|
|
859
947
|
" AND threshold = ? AND alerted_at IS NULL",
|
|
860
|
-
(now_utc_iso(), week_key, project_key, t),
|
|
948
|
+
(as_of or now_utc_iso(), week_key, project_key, t),
|
|
861
949
|
)
|
|
862
|
-
|
|
950
|
+
if own_conn:
|
|
951
|
+
conn.commit()
|
|
863
952
|
finally:
|
|
864
|
-
|
|
953
|
+
if own_conn:
|
|
954
|
+
conn.close()
|
|
865
955
|
except Exception as exc: # best-effort; never fail the write
|
|
866
956
|
eprint(
|
|
867
957
|
f"[project-budget-milestone] reconcile on write failed: {exc}"
|
package/bin/_cctally_parser.py
CHANGED
|
@@ -2834,13 +2834,14 @@ def _build_db_parser(subparsers, name, *, help_text, xref=None):
|
|
|
2834
2834
|
db_unskip.set_defaults(func=c.cmd_db_unskip)
|
|
2835
2835
|
db_recover = db_sub.add_parser(
|
|
2836
2836
|
"recover",
|
|
2837
|
-
help="Revert a version-ahead
|
|
2837
|
+
help="Revert a version-ahead cache.db to the known schema head (#145; "
|
|
2838
|
+
"--db stats is retired — use `db rebuild --db stats`)",
|
|
2838
2839
|
)
|
|
2839
2840
|
db_recover.add_argument(
|
|
2840
2841
|
"--db",
|
|
2841
2842
|
required=True,
|
|
2842
2843
|
choices=("cache", "stats"),
|
|
2843
|
-
help="Which DB to recover",
|
|
2844
|
+
help="Which DB to recover (stats is retired; rebuild it instead)",
|
|
2844
2845
|
)
|
|
2845
2846
|
db_recover.add_argument(
|
|
2846
2847
|
"--yes",
|
|
@@ -2848,9 +2849,26 @@ def _build_db_parser(subparsers, name, *, help_text, xref=None):
|
|
|
2848
2849
|
help="Required for --db stats (non-re-derivable; may need a re-record)",
|
|
2849
2850
|
)
|
|
2850
2851
|
db_recover.set_defaults(func=c.cmd_db_recover)
|
|
2852
|
+
db_rebuild = db_sub.add_parser(
|
|
2853
|
+
"rebuild",
|
|
2854
|
+
help="Rebuild stats.db from the append-only journal (disposable index)",
|
|
2855
|
+
)
|
|
2856
|
+
db_rebuild.add_argument(
|
|
2857
|
+
"--db",
|
|
2858
|
+
required=True,
|
|
2859
|
+
choices=("stats",),
|
|
2860
|
+
help="Database to rebuild (stats only; rebuild cache.db via cache-sync)",
|
|
2861
|
+
)
|
|
2862
|
+
db_rebuild.add_argument(
|
|
2863
|
+
"--json",
|
|
2864
|
+
action="store_true",
|
|
2865
|
+
help="Emit JSON to stdout",
|
|
2866
|
+
)
|
|
2867
|
+
db_rebuild.set_defaults(func=c.cmd_db_rebuild)
|
|
2851
2868
|
db_repair = db_sub.add_parser(
|
|
2852
2869
|
"repair",
|
|
2853
|
-
help="Recover a malformed stats.db through a verified fresh
|
|
2870
|
+
help="Recover a malformed pre-cutover stats.db through a verified fresh "
|
|
2871
|
+
"copy (transitional — post-cutover this folds into journal auto-heal)",
|
|
2854
2872
|
)
|
|
2855
2873
|
db_repair.add_argument(
|
|
2856
2874
|
"--db",
|