cctally 1.117.2 → 1.118.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 +30 -0
- package/README.md +7 -3
- package/bin/_cctally_cache.py +2783 -175
- package/bin/_cctally_completion.py +106 -33
- package/bin/_cctally_core.py +69 -4
- package/bin/_cctally_dashboard.py +188 -13
- package/bin/_cctally_dashboard_conversation.py +5 -1
- package/bin/_cctally_dashboard_envelope.py +50 -19
- package/bin/_cctally_dashboard_sources.py +23 -1
- package/bin/_cctally_db.py +193 -153
- package/bin/_cctally_diagnosis_sources.py +63 -7
- package/bin/_cctally_doctor.py +865 -93
- package/bin/_cctally_five_hour.py +67 -50
- package/bin/_cctally_forecast.py +35 -13
- package/bin/_cctally_journal.py +186 -18
- package/bin/_cctally_milestone_history.py +859 -62
- package/bin/_cctally_oauth_samples.py +286 -15
- package/bin/_cctally_parser.py +2 -2
- package/bin/_cctally_quota.py +104 -12
- package/bin/_cctally_record.py +153 -29
- package/bin/_cctally_rederive.py +608 -68
- package/bin/_cctally_statusline.py +108 -81
- package/bin/_cctally_store.py +179 -38
- package/bin/_cctally_tui.py +892 -181
- package/bin/_cctally_update.py +32 -13
- package/bin/_lib_codex_conversation_query.py +52 -14
- package/bin/_lib_codex_drain_admission.py +224 -6
- package/bin/_lib_codex_metadata.py +39 -0
- package/bin/_lib_conversation_anon.py +15 -8
- package/bin/_lib_conversation_dispatch.py +7 -0
- package/bin/_lib_conversation_query.py +289 -70
- package/bin/_lib_dashboard_sources.py +37 -1
- package/bin/_lib_detector_access_registry.py +497 -26
- package/bin/_lib_doctor.py +85 -20
- package/bin/_lib_doctor_scan_index.py +321 -0
- package/bin/_lib_ingest_frontier.py +20 -1
- package/bin/_lib_oauth_samples.py +23 -7
- package/bin/_lib_pricing.py +146 -16
- package/bin/_lib_quota.py +19 -0
- package/bin/_lib_rederive.py +11 -6
- package/bin/_lib_retained_records.py +516 -0
- package/bin/_lib_snapshot_cache.py +10 -0
- package/bin/_lib_stale_proof.py +272 -43
- package/bin/_lib_stats_content_revision.py +831 -0
- package/bin/_lib_write_io.py +11 -4
- package/bin/cctally +4 -1
- package/dashboard/static/assets/ConversationsView-D4Iv4ku0.js +72 -0
- package/dashboard/static/assets/{DoctorModal-B0DnU9d9.js → DoctorModal-D4QSq6uz.js} +1 -1
- package/dashboard/static/assets/ModalRoot-C9F5dhZi.js +1 -0
- package/dashboard/static/assets/{ProjectsDrillPanel-DSxXQSa-.js → ProjectsDrillPanel-WxAdRDMt.js} +1 -1
- package/dashboard/static/assets/{SourceDetailModal-BleDpQqG.js → SourceDetailModal-NyyEfXmc.js} +1 -1
- package/dashboard/static/assets/{UpdateModal-CMqaAkSK.js → UpdateModal-CFLq9pT1.js} +1 -1
- package/dashboard/static/assets/{index-BjQV-0-D.js → index-BKTWH9JE.js} +2 -2
- package/dashboard/static/assets/{outlineNavigation-CO-VWBm7.js → outlineNavigation-BnGUwCS3.js} +3 -3
- package/dashboard/static/dashboard.html +1 -1
- package/package.json +4 -1
- package/dashboard/static/assets/ConversationsView-oqNsWzWV.js +0 -72
- package/dashboard/static/assets/ModalRoot-CYua2wP_.js +0 -1
package/bin/_cctally_doctor.py
CHANGED
|
@@ -59,6 +59,18 @@ _CONFLICT_SCAN_RECORD_TYPES = _lib_journal_router.RETAINED_RECORD_TYPES
|
|
|
59
59
|
_GUARD_LOG_TAIL_LINES = 256
|
|
60
60
|
_GUARD_LOG_TAIL_BYTES = 64 * 1024
|
|
61
61
|
|
|
62
|
+
#: #1005: the total time one gather may spend waiting on maintenance locks
|
|
63
|
+
#: (every stats open, the open-time guard, and the cache-maintenance
|
|
64
|
+
#: admission), drawn through `_cctally_store.bounded_diagnostic_waits`. Large
|
|
65
|
+
#: enough to absorb a handshake-scale hold, small enough that a held lock is
|
|
66
|
+
#: reported at once instead of costing a full 5 s per independent open.
|
|
67
|
+
_DOCTOR_MAINTENANCE_WAIT_ALLOWANCE_S = 0.5
|
|
68
|
+
|
|
69
|
+
#: #1018: the stats epoch that added `claude_weekly_observation_index`
|
|
70
|
+
#: (#1015). Below it an absent table is expected; at or above it the table is
|
|
71
|
+
#: required, so its absence makes freshness unreadable rather than silent.
|
|
72
|
+
_OBSERVATION_INDEX_EPOCH = 1022
|
|
73
|
+
|
|
62
74
|
|
|
63
75
|
def _cctally():
|
|
64
76
|
"""Resolve the current `cctally` module at call-time (spec §3.1)."""
|
|
@@ -510,7 +522,7 @@ def _gather_dashboard_disk_writes(
|
|
|
510
522
|
directly — no self-HTTP and no recursive gather. Anywhere else (the CLI,
|
|
511
523
|
the TUI) instances are discovered through their owner-readable
|
|
512
524
|
descriptors; a stale one (dead or reused pid) is ignored and never
|
|
513
|
-
deleted, because doctor
|
|
525
|
+
deleted, because doctor never changes your data. Discovery runs only for the
|
|
514
526
|
`cctally doctor` CLI (PR-11); with ``discover`` false nothing is probed and
|
|
515
527
|
the mode is ``not_probed``."""
|
|
516
528
|
import _lib_write_io as wio
|
|
@@ -612,6 +624,513 @@ def _scan_cause(exc: BaseException) -> str:
|
|
|
612
624
|
return f"{text} [{name}]" if name else text
|
|
613
625
|
|
|
614
626
|
|
|
627
|
+
# ── #1054: the deep journal scan's shared per-record step ────────────────
|
|
628
|
+
#
|
|
629
|
+
# The deep scan reads every segment and, for every decoded record, runs the
|
|
630
|
+
# steps below in this order. A checkpointed run (`.doctor-scan-index`, see
|
|
631
|
+
# `bin/_lib_doctor_scan_index.py`) runs them only for the records that
|
|
632
|
+
# *matter* and fills every other position with the placeholder the step
|
|
633
|
+
# itself would have appended. That is exact only because both paths call ONE
|
|
634
|
+
# step function: a change to the step, to `_deep_scan_matters` or to any
|
|
635
|
+
# descriptor input must bump `_lib_doctor_scan_index.SHAPE_VERSION` unless
|
|
636
|
+
# `_deep_scan_index_descriptors` already captures it (docs/journal-gotchas.md).
|
|
637
|
+
|
|
638
|
+
#: The record ids the step acts on regardless of record type: the legacy
|
|
639
|
+
#: accounts cutover (doctor's cutover-candidate capture) and the account-bound
|
|
640
|
+
#: cutover and its two revisions (`_PassBoundary.observe`). Resolved on first
|
|
641
|
+
#: use because `_cctally_journal` is imported call-time.
|
|
642
|
+
_DEEP_SCAN_SPECIAL_IDS = None
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
def _deep_scan_special_ids() -> frozenset:
|
|
646
|
+
global _DEEP_SCAN_SPECIAL_IDS
|
|
647
|
+
if _DEEP_SCAN_SPECIAL_IDS is None:
|
|
648
|
+
import _cctally_journal as _jr
|
|
649
|
+
import _lib_account_protocol
|
|
650
|
+
_DEEP_SCAN_SPECIAL_IDS = frozenset({
|
|
651
|
+
_jr.CUTOVER_OP_ID,
|
|
652
|
+
_lib_account_protocol.CUTOVER_OP_ID,
|
|
653
|
+
_lib_account_protocol.REVISION_OP_ID,
|
|
654
|
+
_lib_account_protocol.REVISION3_OP_ID,
|
|
655
|
+
})
|
|
656
|
+
return _DEEP_SCAN_SPECIAL_IDS
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
def _deep_scan_matters(record: dict) -> bool:
|
|
660
|
+
"""Whether the deep scan's step can do anything for ``record`` beyond
|
|
661
|
+
appending a placeholder: a record the selector retains, or one carrying
|
|
662
|
+
a special id. `id` is checked to be a string before set membership, so
|
|
663
|
+
an unhashable id is simply not special."""
|
|
664
|
+
if record.get("t") in _lib_journal_router.RETAINED_RECORD_TYPES:
|
|
665
|
+
return True
|
|
666
|
+
rid = record.get("id")
|
|
667
|
+
if not isinstance(rid, str):
|
|
668
|
+
return False
|
|
669
|
+
special = _DEEP_SCAN_SPECIAL_IDS
|
|
670
|
+
if special is None:
|
|
671
|
+
special = _deep_scan_special_ids()
|
|
672
|
+
return rid in special
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
class _DeepScanState:
|
|
676
|
+
"""The deep scan's per-pass accumulators, shared by the fresh and the
|
|
677
|
+
checkpointed path. `decoded_records` and `deep_coordinates` are
|
|
678
|
+
positionally parallel: one slot and one coordinate per decoded record,
|
|
679
|
+
the coordinate None for a record that does not matter (its slot is None
|
|
680
|
+
too, so `_apply_pass_admission` skips it either way)."""
|
|
681
|
+
|
|
682
|
+
__slots__ = ("pass_boundary", "decoded_records", "deep_coordinates",
|
|
683
|
+
"protocol_evidence", "protocol_evidence_owners",
|
|
684
|
+
"cutover_candidates", "_capture_evidence", "_cutover_id",
|
|
685
|
+
"_observe", "_append_slot", "_append_coordinate",
|
|
686
|
+
"_append_owner", "_append_candidate", "_selector_slot",
|
|
687
|
+
"_special_ids")
|
|
688
|
+
|
|
689
|
+
def __init__(self):
|
|
690
|
+
import _cctally_journal as _jr
|
|
691
|
+
self.pass_boundary = _jr._PassBoundary()
|
|
692
|
+
self.decoded_records: list = []
|
|
693
|
+
self.deep_coordinates: list = []
|
|
694
|
+
self.protocol_evidence: list = []
|
|
695
|
+
self.protocol_evidence_owners: list = []
|
|
696
|
+
self.cutover_candidates: list = []
|
|
697
|
+
self._capture_evidence = _jr._capture_protocol_prefix_evidence
|
|
698
|
+
self._cutover_id = _jr.CUTOVER_OP_ID
|
|
699
|
+
# #1054: the step runs once per decoded record (millions on a real
|
|
700
|
+
# journal), so it reads these pre-bound slots instead of module
|
|
701
|
+
# globals and attribute chains. Every container is created above and
|
|
702
|
+
# never rebound, so each bound `append` stays its container's.
|
|
703
|
+
self._observe = self.pass_boundary.observe
|
|
704
|
+
self._append_slot = self.decoded_records.append
|
|
705
|
+
self._append_coordinate = self.deep_coordinates.append
|
|
706
|
+
self._append_owner = self.protocol_evidence_owners.append
|
|
707
|
+
self._append_candidate = self.cutover_candidates.append
|
|
708
|
+
self._selector_slot = _lib_journal_router.selector_slot
|
|
709
|
+
self._special_ids = _deep_scan_special_ids()
|
|
710
|
+
|
|
711
|
+
def placeholders(self, count: int) -> None:
|
|
712
|
+
"""``count`` positions for decoded records that do not matter."""
|
|
713
|
+
if count > 0:
|
|
714
|
+
filler = [None] * count
|
|
715
|
+
self.decoded_records.extend(filler)
|
|
716
|
+
self.deep_coordinates.extend(filler)
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
def _deep_scan_step(record: dict, segment: str, offset: int,
|
|
720
|
+
prior_high_water, state: _DeepScanState) -> None:
|
|
721
|
+
"""One decoded record's deep-scan work, in the scan's order: boundary
|
|
722
|
+
observation, protocol-prefix evidence with its owner ordinal, the legacy
|
|
723
|
+
cutover-candidate capture, then the selector slot and the coordinate.
|
|
724
|
+
|
|
725
|
+
"Matters" is computed inline and is exactly `_deep_scan_matters`:
|
|
726
|
+
`selector_slot` returns the record iff its `t` is a retained type, else
|
|
727
|
+
None, and the id test is the same string-then-membership check."""
|
|
728
|
+
state._observe(record)
|
|
729
|
+
evidence = state.protocol_evidence
|
|
730
|
+
state._capture_evidence(record, prior_high_water, evidence)
|
|
731
|
+
if len(evidence) > len(state.protocol_evidence_owners):
|
|
732
|
+
state._append_owner(len(state.decoded_records))
|
|
733
|
+
rid = record.get("id")
|
|
734
|
+
# first cutover op wins, exactly as `find_accounts_cutover_op` scans —
|
|
735
|
+
# captured here so the conflict scan does not decode the whole journal
|
|
736
|
+
# twice.
|
|
737
|
+
if rid == state._cutover_id:
|
|
738
|
+
state._append_candidate((record, segment, offset))
|
|
739
|
+
# RETAIN ONLY what the selector consumes. `obs` lines are ~97% of a real
|
|
740
|
+
# journal and `resolve_effective_events` ignores them entirely — keeping
|
|
741
|
+
# their dictionaries cost 4.3 GB of peak RSS for an identical result
|
|
742
|
+
# (#374 review). They still consume a lightweight slot because their
|
|
743
|
+
# physical sequence is part of three durable violation fingerprints
|
|
744
|
+
# (#508).
|
|
745
|
+
slot = state._selector_slot(record)
|
|
746
|
+
state._append_slot(slot)
|
|
747
|
+
state._append_coordinate(
|
|
748
|
+
(segment, offset)
|
|
749
|
+
if slot is not None
|
|
750
|
+
or (isinstance(rid, str) and rid in state._special_ids)
|
|
751
|
+
else None)
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
#: The deep scan's disposable checkpoint, beside the segments it describes.
|
|
755
|
+
#: `list_segments` lists only `*.jsonl` segment names, so it is invisible to
|
|
756
|
+
#: every journal reader; doctor is its only writer and its only reader.
|
|
757
|
+
_DOCTOR_SCAN_INDEX_NAME = ".doctor-scan-index"
|
|
758
|
+
|
|
759
|
+
|
|
760
|
+
#: A publication temporary older than this is an orphan of a killed doctor:
|
|
761
|
+
#: no live publication holds its temporary file anywhere near this long.
|
|
762
|
+
_DEEP_SCAN_TEMP_MAX_AGE_SECONDS = 3600
|
|
763
|
+
_DEEP_SCAN_TEMP_PREFIX = _DOCTOR_SCAN_INDEX_NAME + "."
|
|
764
|
+
_DEEP_SCAN_TEMP_SUFFIX = ".tmp"
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
def _load_deep_scan_index(jdir: pathlib.Path, version: str):
|
|
768
|
+
"""``(entries, clean)``: the checkpoint's valid entries, or ``{}`` when
|
|
769
|
+
it is absent, foreign or unreadable, and whether the document held
|
|
770
|
+
exactly those entries. Read with `open`, never `Path.read_bytes`, so a
|
|
771
|
+
failure injected into the segment reads cannot reach it (and it cannot
|
|
772
|
+
mask one). Only ordinary exceptions are swallowed."""
|
|
773
|
+
import _lib_doctor_scan_index as _dsi
|
|
774
|
+
try:
|
|
775
|
+
with open(jdir / _DOCTOR_SCAN_INDEX_NAME, "rb") as handle:
|
|
776
|
+
raw = handle.read()
|
|
777
|
+
entries, clean = _dsi.decode_document(raw, version)
|
|
778
|
+
return (entries or {}), clean
|
|
779
|
+
except Exception: # noqa: BLE001 — a missing or bad cache is a cold run
|
|
780
|
+
return {}, False
|
|
781
|
+
|
|
782
|
+
|
|
783
|
+
def _sweep_stale_deep_scan_temps(jdir: pathlib.Path) -> None:
|
|
784
|
+
"""Remove the publication temporaries a killed doctor left behind: only
|
|
785
|
+
names with this module's `mkstemp` prefix and suffix, only regular files
|
|
786
|
+
(never a symlink) and only ones untouched for an hour, so a concurrent
|
|
787
|
+
doctor's live temporary is kept. Best-effort; never creates the
|
|
788
|
+
directory."""
|
|
789
|
+
import stat
|
|
790
|
+
import time
|
|
791
|
+
try:
|
|
792
|
+
names = os.listdir(jdir)
|
|
793
|
+
except OSError:
|
|
794
|
+
return
|
|
795
|
+
cutoff = time.time() - _DEEP_SCAN_TEMP_MAX_AGE_SECONDS
|
|
796
|
+
shortest = len(_DEEP_SCAN_TEMP_PREFIX) + len(_DEEP_SCAN_TEMP_SUFFIX)
|
|
797
|
+
for name in names:
|
|
798
|
+
if (len(name) <= shortest
|
|
799
|
+
or not name.startswith(_DEEP_SCAN_TEMP_PREFIX)
|
|
800
|
+
or not name.endswith(_DEEP_SCAN_TEMP_SUFFIX)):
|
|
801
|
+
continue
|
|
802
|
+
path = os.path.join(jdir, name)
|
|
803
|
+
try:
|
|
804
|
+
info = os.lstat(path)
|
|
805
|
+
if stat.S_ISREG(info.st_mode) and info.st_mtime < cutoff:
|
|
806
|
+
os.unlink(path)
|
|
807
|
+
except OSError:
|
|
808
|
+
continue
|
|
809
|
+
|
|
810
|
+
|
|
811
|
+
def _publish_deep_scan_index(jdir: pathlib.Path, version: str,
|
|
812
|
+
entries: list, previous: dict,
|
|
813
|
+
previous_clean: bool) -> None:
|
|
814
|
+
"""Atomically replace the checkpoint with ``entries`` (spec 5.5).
|
|
815
|
+
|
|
816
|
+
Stale temporaries of killed doctors are swept first. When the loaded
|
|
817
|
+
document was clean and holds exactly ``entries`` (``previous``) the
|
|
818
|
+
write is skipped, so an idle doctor does not rewrite a multi-MB file.
|
|
819
|
+
Otherwise a unique temporary file in the journal directory, mode 0600,
|
|
820
|
+
then `os.replace`: no fsync (the file is disposable and every entry is
|
|
821
|
+
verified on reuse) and no lock (concurrent doctors are last-writer-wins).
|
|
822
|
+
The journal directory is never created. Any ordinary failure is
|
|
823
|
+
swallowed and leaves no temporary file, so the run's output and exit
|
|
824
|
+
status never depend on it."""
|
|
825
|
+
import tempfile
|
|
826
|
+
import _lib_doctor_scan_index as _dsi
|
|
827
|
+
tmp_path = None
|
|
828
|
+
try:
|
|
829
|
+
_sweep_stale_deep_scan_temps(jdir)
|
|
830
|
+
if (previous_clean and len(entries) == len(previous)
|
|
831
|
+
and all(previous.get(entry.name) == entry
|
|
832
|
+
for entry in entries)):
|
|
833
|
+
return
|
|
834
|
+
payload = _dsi.encode(version, entries)
|
|
835
|
+
fd, tmp_path = tempfile.mkstemp(
|
|
836
|
+
dir=str(jdir), prefix=_DEEP_SCAN_TEMP_PREFIX,
|
|
837
|
+
suffix=_DEEP_SCAN_TEMP_SUFFIX)
|
|
838
|
+
try:
|
|
839
|
+
view = memoryview(payload)
|
|
840
|
+
while view:
|
|
841
|
+
view = view[os.write(fd, view):]
|
|
842
|
+
finally:
|
|
843
|
+
os.close(fd)
|
|
844
|
+
os.replace(tmp_path, jdir / _DOCTOR_SCAN_INDEX_NAME)
|
|
845
|
+
tmp_path = None
|
|
846
|
+
except Exception: # noqa: BLE001 — the checkpoint is best-effort
|
|
847
|
+
pass
|
|
848
|
+
finally:
|
|
849
|
+
if tmp_path is not None:
|
|
850
|
+
try:
|
|
851
|
+
os.unlink(tmp_path)
|
|
852
|
+
except OSError:
|
|
853
|
+
pass
|
|
854
|
+
|
|
855
|
+
|
|
856
|
+
def _deep_decode_hits(data: bytes, entry) -> "list | None":
|
|
857
|
+
"""The decoded hit records of a verified prefix, or None when any hit
|
|
858
|
+
does not start a line, does not decode or does not matter — checked for
|
|
859
|
+
every hit before the caller touches any scan state."""
|
|
860
|
+
import _lib_journal as _jl
|
|
861
|
+
records = []
|
|
862
|
+
for offset, _ordinal in entry.hits:
|
|
863
|
+
if offset and data[offset - 1] != 0x0A:
|
|
864
|
+
return None
|
|
865
|
+
newline = data.find(b"\n", offset, entry.covered)
|
|
866
|
+
if newline <= offset:
|
|
867
|
+
return None
|
|
868
|
+
record = _jl.decode_line(data[offset:newline])
|
|
869
|
+
if record is None or not _deep_scan_matters(record):
|
|
870
|
+
return None
|
|
871
|
+
records.append(record)
|
|
872
|
+
return records
|
|
873
|
+
|
|
874
|
+
|
|
875
|
+
def _deep_scan_segment(segment: str, data: bytes, entry, prior_high_water,
|
|
876
|
+
state: _DeepScanState):
|
|
877
|
+
"""Scan one segment's bytes; return ``(new entry, malformed lines,
|
|
878
|
+
prior high-water)``.
|
|
879
|
+
|
|
880
|
+
With a usable checkpoint entry (its prefix verified against ``data`` and
|
|
881
|
+
every hit decoding to a record that matters) the cached prefix
|
|
882
|
+
contributes its malformed count, the shared step for each hit and a
|
|
883
|
+
placeholder for every other decoded position; the running prior
|
|
884
|
+
high-water then becomes ``(segment, covered)``, the value the fresh loop
|
|
885
|
+
holds at the first suffix line. Everything from ``covered`` on — or the
|
|
886
|
+
whole segment without a usable entry — is processed exactly like the
|
|
887
|
+
fresh loop. The prefix hash is continued over the suffix, so each byte
|
|
888
|
+
is hashed once. A torn tail is never covered and never stored."""
|
|
889
|
+
import hashlib
|
|
890
|
+
import _lib_journal as _jl
|
|
891
|
+
import _lib_doctor_scan_index as _dsi
|
|
892
|
+
end = data.rfind(b"\n") + 1
|
|
893
|
+
hasher = None
|
|
894
|
+
hit_records = None
|
|
895
|
+
if entry is not None:
|
|
896
|
+
hasher = _dsi.verify_prefix(entry.sha256, entry.covered, data)
|
|
897
|
+
if hasher is not None:
|
|
898
|
+
hit_records = _deep_decode_hits(data, entry)
|
|
899
|
+
if hit_records is None:
|
|
900
|
+
hasher = None
|
|
901
|
+
if hasher is not None:
|
|
902
|
+
malformed = entry.malformed
|
|
903
|
+
position = 0
|
|
904
|
+
for (offset, ordinal), record in zip(entry.hits, hit_records):
|
|
905
|
+
state.placeholders(ordinal - position)
|
|
906
|
+
_deep_scan_step(
|
|
907
|
+
record, segment, offset,
|
|
908
|
+
(segment, offset) if offset > 0 else prior_high_water,
|
|
909
|
+
state)
|
|
910
|
+
position = ordinal + 1
|
|
911
|
+
state.placeholders(entry.decoded - position)
|
|
912
|
+
decoded = entry.decoded
|
|
913
|
+
hits = list(entry.hits)
|
|
914
|
+
start = entry.covered
|
|
915
|
+
if start > 0:
|
|
916
|
+
prior_high_water = (segment, start)
|
|
917
|
+
else:
|
|
918
|
+
hasher = hashlib.sha256()
|
|
919
|
+
malformed = 0
|
|
920
|
+
decoded = 0
|
|
921
|
+
hits = []
|
|
922
|
+
start = 0
|
|
923
|
+
hasher.update(memoryview(data)[start:end])
|
|
924
|
+
coordinates = state.deep_coordinates
|
|
925
|
+
# #1054: the loop below runs once per journal line, so its callables are
|
|
926
|
+
# locals. The step is read from the module global at call time, not at
|
|
927
|
+
# import, so a patched `_deep_scan_step` still intercepts every record.
|
|
928
|
+
decode_line = _jl.decode_line
|
|
929
|
+
step = _deep_scan_step
|
|
930
|
+
append_hit = hits.append
|
|
931
|
+
# every element except the last is a complete line; the last is either
|
|
932
|
+
# "" (ended in \n) or the torn partial — not a mid-file line, so it is
|
|
933
|
+
# never counted as malformed.
|
|
934
|
+
offset = start
|
|
935
|
+
for raw in (data if start == 0 else data[start:]).split(b"\n")[:-1]:
|
|
936
|
+
if not raw:
|
|
937
|
+
prior_high_water = (segment, offset + 1)
|
|
938
|
+
offset += 1
|
|
939
|
+
continue
|
|
940
|
+
record = decode_line(raw)
|
|
941
|
+
if record is None:
|
|
942
|
+
malformed += 1
|
|
943
|
+
prior_high_water = (segment, offset + len(raw) + 1)
|
|
944
|
+
offset += len(raw) + 1
|
|
945
|
+
continue
|
|
946
|
+
step(record, segment, offset, prior_high_water, state)
|
|
947
|
+
if coordinates[-1] is not None:
|
|
948
|
+
append_hit((offset, decoded))
|
|
949
|
+
decoded += 1
|
|
950
|
+
prior_high_water = (segment, offset + len(raw) + 1)
|
|
951
|
+
offset += len(raw) + 1
|
|
952
|
+
if end > 0:
|
|
953
|
+
prior_high_water = (segment, end)
|
|
954
|
+
new_entry = _dsi.Entry(segment, end, hasher.hexdigest(), decoded,
|
|
955
|
+
malformed, tuple(hits), None)
|
|
956
|
+
return new_entry, malformed, prior_high_water
|
|
957
|
+
|
|
958
|
+
|
|
959
|
+
def _deep_boundary_fields(boundary) -> "dict | None":
|
|
960
|
+
"""The admission boundary as the explicit fields `classify_admission`
|
|
961
|
+
reads (spec 5.4): the cutover's position and frozen extents, and for
|
|
962
|
+
each attached revision its presence, position, frozen extents and
|
|
963
|
+
linkage. Spool baselines and inventory evidence are not admission
|
|
964
|
+
inputs. The dataclass equality is deliberately not used: it ignores the
|
|
965
|
+
attached revisions."""
|
|
966
|
+
if boundary is None:
|
|
967
|
+
return None
|
|
968
|
+
|
|
969
|
+
def extents(fence):
|
|
970
|
+
return sorted([str(seg), int(end)]
|
|
971
|
+
for seg, end in fence.frozen_extents.items())
|
|
972
|
+
|
|
973
|
+
revision = boundary.revision
|
|
974
|
+
revision3 = boundary.revision3
|
|
975
|
+
return {
|
|
976
|
+
"cutover": {"segment": boundary.segment,
|
|
977
|
+
"end_offset": boundary.end_offset,
|
|
978
|
+
"frozen_extents": extents(boundary)},
|
|
979
|
+
"revision": None if revision is None else {
|
|
980
|
+
"segment": revision.segment,
|
|
981
|
+
"end_offset": revision.end_offset,
|
|
982
|
+
"frozen_extents": extents(revision),
|
|
983
|
+
"cutover_segment": revision.cutover_segment,
|
|
984
|
+
"cutover_offset": revision.cutover_offset,
|
|
985
|
+
},
|
|
986
|
+
"revision3": None if revision3 is None else {
|
|
987
|
+
"segment": revision3.segment,
|
|
988
|
+
"end_offset": revision3.end_offset,
|
|
989
|
+
"frozen_extents": extents(revision3),
|
|
990
|
+
"cutover_segment": revision3.cutover_segment,
|
|
991
|
+
"cutover_offset": revision3.cutover_offset,
|
|
992
|
+
"revision2_segment": revision3.revision2_segment,
|
|
993
|
+
"revision2_offset": revision3.revision2_offset,
|
|
994
|
+
},
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
|
|
998
|
+
def _deep_exclusion_read(path, segment: str, lo: int, hi: int, boundary,
|
|
999
|
+
cached):
|
|
1000
|
+
"""One read of ``[lo, hi)`` classifying excluded records; returns
|
|
1001
|
+
``(excluded records, hasher)``, or None when ``cached`` was offered and
|
|
1002
|
+
could not be used.
|
|
1003
|
+
|
|
1004
|
+
With ``cached`` the `RangeHasher` proves ``[lo, cached.covered)`` from
|
|
1005
|
+
the very chunks this read classifies: lines starting before ``covered``
|
|
1006
|
+
are not decoded, lines from ``covered`` on are decoded only once the
|
|
1007
|
+
comparison matched, and a mismatch — or a stream that ended before
|
|
1008
|
+
reaching ``covered`` — abandons the read. Prefix lines are never
|
|
1009
|
+
buffered, so the iterator's one-chunk memory bound holds."""
|
|
1010
|
+
import _cctally_journal as _jr
|
|
1011
|
+
import _lib_account_protocol
|
|
1012
|
+
import _lib_doctor_scan_index as _dsi
|
|
1013
|
+
import _lib_journal as _jl
|
|
1014
|
+
hasher = _dsi.RangeHasher(
|
|
1015
|
+
lo, None if cached is None else cached.covered,
|
|
1016
|
+
None if cached is None else cached.sha256)
|
|
1017
|
+
covered = lo if cached is None else cached.covered
|
|
1018
|
+
out = []
|
|
1019
|
+
lines = _jr._iter_segment_lines(path, lo, hi, on_bytes=hasher)
|
|
1020
|
+
try:
|
|
1021
|
+
for _name, offset, raw in lines:
|
|
1022
|
+
if cached is not None:
|
|
1023
|
+
if hasher.prefix_state == "mismatch":
|
|
1024
|
+
return None
|
|
1025
|
+
if offset < covered:
|
|
1026
|
+
continue
|
|
1027
|
+
record = _jl.decode_line(raw)
|
|
1028
|
+
if record is None:
|
|
1029
|
+
continue
|
|
1030
|
+
point = _lib_account_protocol.Coordinate(segment, offset)
|
|
1031
|
+
verdict = _lib_account_protocol.classify_admission(
|
|
1032
|
+
record, point, boundary)
|
|
1033
|
+
if not verdict.admitted:
|
|
1034
|
+
out.append(_jr.ExcludedRecord(coordinate=point,
|
|
1035
|
+
family=verdict.family))
|
|
1036
|
+
finally:
|
|
1037
|
+
lines.close()
|
|
1038
|
+
if cached is not None:
|
|
1039
|
+
if hasher.prefix_state != "match":
|
|
1040
|
+
return None
|
|
1041
|
+
out = [
|
|
1042
|
+
_jr.ExcludedRecord(
|
|
1043
|
+
coordinate=_lib_account_protocol.Coordinate(segment, offset),
|
|
1044
|
+
family=family)
|
|
1045
|
+
for offset, family in cached.excluded
|
|
1046
|
+
] + out
|
|
1047
|
+
return out, hasher
|
|
1048
|
+
|
|
1049
|
+
|
|
1050
|
+
def _deep_excluded_population(boundary, index: dict,
|
|
1051
|
+
contributions: dict) -> list:
|
|
1052
|
+
"""`_cctally_journal.excluded_population` with per-segment reuse (#1054
|
|
1053
|
+
spec 5.4); same contract and observation point.
|
|
1054
|
+
|
|
1055
|
+
It re-lists and re-stats the segments at call time, so it sees bytes
|
|
1056
|
+
and segments appended after the main pass. Per segment the range is
|
|
1057
|
+
``[frozen extent, complete-line end)``; a segment removed since the
|
|
1058
|
+
listing is skipped and any other failure raises to the caller's
|
|
1059
|
+
unavailable handling. A cached exclusion is offered only when its
|
|
1060
|
+
boundary fingerprint and ``lo`` match and its ``covered`` lies in the
|
|
1061
|
+
current range; when it cannot be used the segment is recomputed cold
|
|
1062
|
+
by a second read. A contribution is recorded in ``contributions`` only
|
|
1063
|
+
from a read that consumed exactly ``hi - lo`` bytes."""
|
|
1064
|
+
import _cctally_journal as _jr
|
|
1065
|
+
import _lib_doctor_scan_index as _dsi
|
|
1066
|
+
import _lib_journal as _jl
|
|
1067
|
+
if boundary is None:
|
|
1068
|
+
return []
|
|
1069
|
+
fingerprint = _dsi.boundary_fingerprint(_deep_boundary_fields(boundary))
|
|
1070
|
+
out = []
|
|
1071
|
+
for seg in sorted(_jr._segment_sizes(), key=_jl.segment_sort_key):
|
|
1072
|
+
path = _cctally_core.JOURNAL_DIR / seg
|
|
1073
|
+
lo = boundary.frozen_extents.get(seg, 0)
|
|
1074
|
+
try:
|
|
1075
|
+
hi = _jr._complete_line_offset(path, os.path.getsize(path))
|
|
1076
|
+
except FileNotFoundError:
|
|
1077
|
+
continue
|
|
1078
|
+
if hi <= lo:
|
|
1079
|
+
continue
|
|
1080
|
+
entry = index.get(seg)
|
|
1081
|
+
cached = None if entry is None else entry.exclusion
|
|
1082
|
+
if cached is not None and not (
|
|
1083
|
+
cached.boundary == fingerprint and cached.lo == lo
|
|
1084
|
+
and lo <= cached.covered <= hi):
|
|
1085
|
+
cached = None
|
|
1086
|
+
result = None
|
|
1087
|
+
if cached is not None:
|
|
1088
|
+
result = _deep_exclusion_read(path, seg, lo, hi, boundary, cached)
|
|
1089
|
+
if result is None:
|
|
1090
|
+
result = _deep_exclusion_read(path, seg, lo, hi, boundary, None)
|
|
1091
|
+
excluded, hasher = result
|
|
1092
|
+
out.extend(excluded)
|
|
1093
|
+
if hasher.consumed == hi - lo:
|
|
1094
|
+
contributions[seg] = _dsi.Exclusion(
|
|
1095
|
+
fingerprint, lo, hi, hasher.digest(),
|
|
1096
|
+
tuple((item.coordinate.offset, item.family)
|
|
1097
|
+
for item in excluded))
|
|
1098
|
+
return out
|
|
1099
|
+
|
|
1100
|
+
|
|
1101
|
+
def _deep_scan_index_descriptors() -> dict:
|
|
1102
|
+
"""Every derived input of the checkpoint version (spec 5.2): the
|
|
1103
|
+
retained record types, the special ids, the protocol-resolution kinds,
|
|
1104
|
+
each constant `_lib_account_protocol.classify_admission` and its helpers
|
|
1105
|
+
read, and the stats epoch. `CCTALLY_VERSION` is deliberately absent: a
|
|
1106
|
+
release that changes none of these keeps every checkpoint."""
|
|
1107
|
+
import _lib_account_protocol as ap
|
|
1108
|
+
import _lib_journal
|
|
1109
|
+
return {
|
|
1110
|
+
"retained_record_types": sorted(
|
|
1111
|
+
_lib_journal_router.RETAINED_RECORD_TYPES),
|
|
1112
|
+
"special_ids": sorted(_deep_scan_special_ids()),
|
|
1113
|
+
"protocol_resolution_kinds": sorted(
|
|
1114
|
+
_lib_journal.PROTOCOL_RESOLUTION_KINDS),
|
|
1115
|
+
"admission": {
|
|
1116
|
+
"writer_generation_key": ap.WRITER_GENERATION_KEY,
|
|
1117
|
+
"protocol_generation": ap.PROTOCOL_GENERATION,
|
|
1118
|
+
"revision_key": ap.REVISION_KEY,
|
|
1119
|
+
"protocol_revision": ap.PROTOCOL_REVISION,
|
|
1120
|
+
"successor_revision_key": ap.SUCCESSOR_REVISION_KEY,
|
|
1121
|
+
"successor_revision": ap.SUCCESSOR_REVISION,
|
|
1122
|
+
"administrative_record_types": sorted(ap._ADMIN_RECORD_TYPES),
|
|
1123
|
+
"claude_op_kinds": sorted(ap._CLAUDE_OP_KINDS),
|
|
1124
|
+
"codex_op_kinds": sorted(ap._CODEX_OP_KINDS),
|
|
1125
|
+
"administrative_op_kinds": sorted(ap._ADMIN_OP_KINDS),
|
|
1126
|
+
"protocol_op_kinds": sorted(ap._PROTOCOL_OP_KINDS),
|
|
1127
|
+
"claude_evt_kinds": sorted(ap._CLAUDE_EVT_KINDS),
|
|
1128
|
+
"quota_evt_kinds": sorted(ap._QUOTA_EVT_KINDS),
|
|
1129
|
+
},
|
|
1130
|
+
"stats_index_epoch": _cctally_core.STATS_INDEX_EPOCH,
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
|
|
615
1134
|
def _durable_inventory_marker(conn) -> bool:
|
|
616
1135
|
"""Whether the validated index shows the legacy spool inventory episode
|
|
617
1136
|
open (#948 spec 7.3: the `journal.legacy_writer` addendum says detection
|
|
@@ -938,8 +1457,9 @@ def _codex_quota_verify_activity_24h(*, now_utc: "dt.datetime") -> dict:
|
|
|
938
1457
|
|
|
939
1458
|
#: How much journal tail the `accounts.codex_window_attribution` leg will read
|
|
940
1459
|
#: to decide whether an attribution op is sitting unconsumed. Past this, the
|
|
941
|
-
#: condition is left unreported: `doctor` is a
|
|
942
|
-
#: ingest, and the state it would have reported
|
|
1460
|
+
#: condition is left unreported: `doctor` is a health report that never
|
|
1461
|
+
#: changes your data, not an ingest, and the state it would have reported
|
|
1462
|
+
#: self-heals on the next sync.
|
|
943
1463
|
_WINDOW_ATTRIBUTION_TAIL_BUDGET_BYTES = 8 * 1024 * 1024
|
|
944
1464
|
|
|
945
1465
|
#: The same discipline on the same leg's OTHER unbounded read. Every distinct
|
|
@@ -1002,7 +1522,6 @@ def _gather_codex_window_attribution_state() -> "dict | None":
|
|
|
1002
1522
|
return None
|
|
1003
1523
|
try:
|
|
1004
1524
|
import _cctally_cache
|
|
1005
|
-
import _cctally_journal
|
|
1006
1525
|
import _cctally_quota
|
|
1007
1526
|
import _lib_codex_window_attribution as _wa
|
|
1008
1527
|
import _lib_journal
|
|
@@ -1046,7 +1565,16 @@ def _gather_codex_window_attribution_state() -> "dict | None":
|
|
|
1046
1565
|
# is left unreported rather than guessed at, because it self-heals on
|
|
1047
1566
|
# the next sync either way.
|
|
1048
1567
|
cursor = _cctally_cache.load_codex_window_attribution_cursor(conn)
|
|
1049
|
-
|
|
1568
|
+
# #1005: lock-free, like `stats_interrupted_rebuild_evidence`. The
|
|
1569
|
+
# leaf-locked `journal_high_water()` blocks while a writer holds
|
|
1570
|
+
# `journal.lock` and creates that file, and a read-only gather must
|
|
1571
|
+
# do neither; the scan below already tolerates a torn tail line.
|
|
1572
|
+
try:
|
|
1573
|
+
import _cctally_store
|
|
1574
|
+
|
|
1575
|
+
high_water = _cctally_store._lock_free_journal_high_water()
|
|
1576
|
+
except OSError:
|
|
1577
|
+
high_water = None
|
|
1050
1578
|
if cursor is not None and high_water is not None:
|
|
1051
1579
|
state["cursor_behind"] = _unconsumed_window_attribution_ops(
|
|
1052
1580
|
cursor, high_water)
|
|
@@ -1657,6 +2185,204 @@ def _read_rollup_pricing_state(conn) -> "tuple[dict | None, str | None, object]"
|
|
|
1657
2185
|
return record, stored_fp, observation
|
|
1658
2186
|
|
|
1659
2187
|
|
|
2188
|
+
@contextlib.contextmanager
|
|
2189
|
+
def _freshness_read_transaction(conn):
|
|
2190
|
+
"""One deferred read transaction around the snapshot selection (#1018).
|
|
2191
|
+
|
|
2192
|
+
The tie-group selection runs several statements; without one read view a
|
|
2193
|
+
publication or delete between them could empty J1's group, and the
|
|
2194
|
+
fallback would then wrongly treat it as all-future. Always ended here, so
|
|
2195
|
+
no shared lock outlives the selection into the connection's later probes.
|
|
2196
|
+
"""
|
|
2197
|
+
conn.execute("BEGIN")
|
|
2198
|
+
try:
|
|
2199
|
+
yield
|
|
2200
|
+
finally:
|
|
2201
|
+
if conn.in_transaction:
|
|
2202
|
+
conn.rollback()
|
|
2203
|
+
|
|
2204
|
+
|
|
2205
|
+
def _stats_table_exists(conn, name: str) -> bool:
|
|
2206
|
+
"""Table presence from `sqlite_master`, never from a caught error: a
|
|
2207
|
+
`database is locked` is an `OperationalError` too."""
|
|
2208
|
+
return conn.execute(
|
|
2209
|
+
"SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?",
|
|
2210
|
+
(name,),
|
|
2211
|
+
).fetchone() is not None
|
|
2212
|
+
|
|
2213
|
+
|
|
2214
|
+
def _parse_capture_utc(value, label: str) -> dt.datetime:
|
|
2215
|
+
"""A stored capture spelling as an aware UTC instant.
|
|
2216
|
+
|
|
2217
|
+
A spelling without an offset is UTC, never host time: the selection's
|
|
2218
|
+
tie groups come from SQLite's ``julianday()``, which reads it as UTC, so
|
|
2219
|
+
a host-local parse would report a moment SQLite never grouped.
|
|
2220
|
+
"""
|
|
2221
|
+
if not isinstance(value, str):
|
|
2222
|
+
raise ValueError(f"{label} is not an ISO datetime: {value!r}")
|
|
2223
|
+
text = value.strip()
|
|
2224
|
+
try:
|
|
2225
|
+
parsed = dt.datetime.fromisoformat(text.replace("Z", "+00:00"))
|
|
2226
|
+
except ValueError as exc:
|
|
2227
|
+
raise ValueError(f"{label} must be ISO datetime") from exc
|
|
2228
|
+
if parsed.tzinfo is None:
|
|
2229
|
+
parsed = parsed.replace(tzinfo=dt.timezone.utc)
|
|
2230
|
+
return parsed.astimezone(dt.timezone.utc)
|
|
2231
|
+
|
|
2232
|
+
|
|
2233
|
+
def _latest_snapshot_capture(conn, now_utc: dt.datetime):
|
|
2234
|
+
"""Newest held-inclusive `weekly_usage_snapshots` capture <= ``now_utc``.
|
|
2235
|
+
|
|
2236
|
+
Chronological, because the writer keeps its ISO spelling (offsets, mixed
|
|
2237
|
+
precision): text MAX is not. ``julianday`` buckets by millisecond, so the
|
|
2238
|
+
choice is exact by tie groups (spec 5.3): the newest bucket <= now, every
|
|
2239
|
+
row of it parsed and filtered against ``now_utc``; if that bucket is all
|
|
2240
|
+
after now, the next older bucket, all of whose rows precede now. Raises
|
|
2241
|
+
``sqlite3.Error``/``ValueError`` when the source cannot be read.
|
|
2242
|
+
"""
|
|
2243
|
+
if not _stats_table_exists(conn, "weekly_usage_snapshots"):
|
|
2244
|
+
return None # no snapshots yet
|
|
2245
|
+
label = "weekly_usage_snapshots.captured_at_utc"
|
|
2246
|
+
now_jd = now_utc.astimezone(dt.timezone.utc).isoformat()
|
|
2247
|
+
|
|
2248
|
+
def _group(julian):
|
|
2249
|
+
rows = conn.execute(
|
|
2250
|
+
"SELECT captured_at_utc FROM weekly_usage_snapshots "
|
|
2251
|
+
"WHERE julianday(captured_at_utc) = ?",
|
|
2252
|
+
(julian,),
|
|
2253
|
+
).fetchall()
|
|
2254
|
+
return [
|
|
2255
|
+
moment for moment in
|
|
2256
|
+
(_parse_capture_utc(row[0], label) for row in rows)
|
|
2257
|
+
if moment <= now_utc
|
|
2258
|
+
]
|
|
2259
|
+
|
|
2260
|
+
with _freshness_read_transaction(conn):
|
|
2261
|
+
# fetchall, so each statement is exhausted and reset before the next.
|
|
2262
|
+
j1 = conn.execute(
|
|
2263
|
+
"SELECT MAX(julianday(captured_at_utc)) FROM weekly_usage_snapshots "
|
|
2264
|
+
"WHERE julianday(captured_at_utc) <= julianday(?)",
|
|
2265
|
+
(now_jd,),
|
|
2266
|
+
).fetchall()[0][0]
|
|
2267
|
+
if j1 is None:
|
|
2268
|
+
return None
|
|
2269
|
+
valid = _group(j1)
|
|
2270
|
+
if valid:
|
|
2271
|
+
return max(valid)
|
|
2272
|
+
j2 = conn.execute(
|
|
2273
|
+
"SELECT MAX(julianday(captured_at_utc)) FROM weekly_usage_snapshots "
|
|
2274
|
+
"WHERE julianday(captured_at_utc) < ?",
|
|
2275
|
+
(j1,),
|
|
2276
|
+
).fetchall()[0][0]
|
|
2277
|
+
if j2 is None:
|
|
2278
|
+
return None
|
|
2279
|
+
valid = _group(j2)
|
|
2280
|
+
return max(valid) if valid else None
|
|
2281
|
+
|
|
2282
|
+
|
|
2283
|
+
def _latest_admitted_reading(conn, now_utc: dt.datetime):
|
|
2284
|
+
"""Newest admitted Claude weekly reading <= ``now_utc`` (#1018).
|
|
2285
|
+
|
|
2286
|
+
`claude_weekly_observation_index` holds one row per admitted observation
|
|
2287
|
+
carrying a weekly reading, projected before snapshot dedup, and stores the
|
|
2288
|
+
fixed-width canonical instant, so text comparison is exact chronological
|
|
2289
|
+
comparison. A store below the epoch that added it has no table and falls
|
|
2290
|
+
back to snapshots; a current store missing it is unreadable.
|
|
2291
|
+
"""
|
|
2292
|
+
if not _stats_table_exists(conn, "claude_weekly_observation_index"):
|
|
2293
|
+
epoch = conn.execute("PRAGMA user_version").fetchone()[0]
|
|
2294
|
+
if int(epoch or 0) < _OBSERVATION_INDEX_EPOCH:
|
|
2295
|
+
return None
|
|
2296
|
+
raise sqlite3.DatabaseError(
|
|
2297
|
+
"claude_weekly_observation_index is missing from a "
|
|
2298
|
+
f"current-epoch stats.db (user_version {epoch})"
|
|
2299
|
+
)
|
|
2300
|
+
row = conn.execute(
|
|
2301
|
+
"SELECT MAX(captured_at_utc) FROM claude_weekly_observation_index "
|
|
2302
|
+
"WHERE captured_at_utc <= ?",
|
|
2303
|
+
(now_utc.astimezone(dt.timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ"),),
|
|
2304
|
+
).fetchone()
|
|
2305
|
+
if row is None or row[0] is None:
|
|
2306
|
+
return None
|
|
2307
|
+
return _parse_capture_utc(
|
|
2308
|
+
row[0], "claude_weekly_observation_index.captured_at_utc"
|
|
2309
|
+
)
|
|
2310
|
+
|
|
2311
|
+
|
|
2312
|
+
def _gather_freshness_sources(conn, now_utc: dt.datetime):
|
|
2313
|
+
"""``(latest_snapshot_at, latest_reading_at, reason)`` for one gather.
|
|
2314
|
+
|
|
2315
|
+
Each source runs independently, so one failure keeps the other's value;
|
|
2316
|
+
``reason`` is ``"stats_unreadable"`` when either could not be read.
|
|
2317
|
+
"""
|
|
2318
|
+
unreadable = False
|
|
2319
|
+
latest_snapshot_at = latest_reading_at = None
|
|
2320
|
+
try:
|
|
2321
|
+
latest_snapshot_at = _latest_snapshot_capture(conn, now_utc)
|
|
2322
|
+
except (sqlite3.Error, ValueError):
|
|
2323
|
+
unreadable = True
|
|
2324
|
+
try:
|
|
2325
|
+
latest_reading_at = _latest_admitted_reading(conn, now_utc)
|
|
2326
|
+
except (sqlite3.Error, ValueError):
|
|
2327
|
+
unreadable = True
|
|
2328
|
+
return (
|
|
2329
|
+
latest_snapshot_at,
|
|
2330
|
+
latest_reading_at,
|
|
2331
|
+
"stats_unreadable" if unreadable else None,
|
|
2332
|
+
)
|
|
2333
|
+
|
|
2334
|
+
|
|
2335
|
+
def _stats_maintenance_lock_refused() -> bool:
|
|
2336
|
+
"""True when ``stats.db.maintenance.lock`` exists and a non-blocking
|
|
2337
|
+
shared probe is refused. Read-only: no ``O_CREAT``, never waits."""
|
|
2338
|
+
try:
|
|
2339
|
+
fd = os.open(str(_cctally_core.STATS_LOCK_MAINTENANCE_PATH), os.O_RDONLY)
|
|
2340
|
+
except OSError:
|
|
2341
|
+
return False
|
|
2342
|
+
try:
|
|
2343
|
+
try:
|
|
2344
|
+
fcntl.flock(fd, fcntl.LOCK_SH | fcntl.LOCK_NB)
|
|
2345
|
+
except BlockingIOError:
|
|
2346
|
+
return True
|
|
2347
|
+
except OSError:
|
|
2348
|
+
return False
|
|
2349
|
+
fcntl.flock(fd, fcntl.LOCK_UN)
|
|
2350
|
+
return False
|
|
2351
|
+
finally:
|
|
2352
|
+
os.close(fd)
|
|
2353
|
+
|
|
2354
|
+
|
|
2355
|
+
def _classify_absent_stats(stats_db_status) -> "str | None":
|
|
2356
|
+
"""Why an absent stats.db yields no freshness evidence (spec 5.3).
|
|
2357
|
+
|
|
2358
|
+
During a rebuild's physical publication the old family can be quarantined
|
|
2359
|
+
before its replacement is installed, so an absent file is a fresh install
|
|
2360
|
+
only when no maintenance evidence says otherwise.
|
|
2361
|
+
"""
|
|
2362
|
+
import _cctally_store
|
|
2363
|
+
|
|
2364
|
+
status = stats_db_status if isinstance(stats_db_status, dict) else {}
|
|
2365
|
+
interrupted = status.get("_interrupted_rebuild")
|
|
2366
|
+
if interrupted:
|
|
2367
|
+
if interrupted.get("live") is True:
|
|
2368
|
+
return "stats_maintenance"
|
|
2369
|
+
return "stats_unreadable" # db.stats.file names the recovery
|
|
2370
|
+
if status.get("_interrupted_inspection_error"):
|
|
2371
|
+
# An inspection failure never reads as a fresh install.
|
|
2372
|
+
if _stats_maintenance_lock_refused():
|
|
2373
|
+
return "stats_maintenance"
|
|
2374
|
+
return "stats_unreadable"
|
|
2375
|
+
db_path = _cctally_core.DB_PATH
|
|
2376
|
+
if (
|
|
2377
|
+
_cctally_store._cctally_db._quarantine_pending_path(db_path).exists()
|
|
2378
|
+
or _cctally_store._stats_repair_marker(db_path).exists()
|
|
2379
|
+
):
|
|
2380
|
+
return "stats_maintenance"
|
|
2381
|
+
if _stats_maintenance_lock_refused():
|
|
2382
|
+
return "stats_maintenance"
|
|
2383
|
+
return None
|
|
2384
|
+
|
|
2385
|
+
|
|
1660
2386
|
def doctor_gather_state(
|
|
1661
2387
|
*,
|
|
1662
2388
|
now_utc: "dt.datetime | None" = None,
|
|
@@ -1667,11 +2393,12 @@ def doctor_gather_state(
|
|
|
1667
2393
|
):
|
|
1668
2394
|
"""Gather doctor state while excluding cache-family replacement.
|
|
1669
2395
|
|
|
1670
|
-
Doctor
|
|
1671
|
-
lock already exists it holds maintenance-shared from the
|
|
1672
|
-
through every cache probe. A live/stale marker or a pending
|
|
1673
|
-
suppresses all raw SQLite cache opens; when a cache exists but
|
|
1674
|
-
absent, probes also degrade rather than racing a newly
|
|
2396
|
+
Doctor never changes your data, so it never creates the maintenance
|
|
2397
|
+
lock. When the lock already exists it holds maintenance-shared from the
|
|
2398
|
+
marker check through every cache probe. A live/stale marker or a pending
|
|
2399
|
+
quarantine suppresses all raw SQLite cache opens; when a cache exists but
|
|
2400
|
+
its lock is absent, probes also degrade rather than racing a newly
|
|
2401
|
+
starting repair.
|
|
1675
2402
|
|
|
1676
2403
|
``force_cold_inputs`` bypasses every reused raw input (#583 S5 §2.3), so a
|
|
1677
2404
|
caller can obtain a genuinely fresh gather at a chosen instant. It exists
|
|
@@ -1683,6 +2410,28 @@ def doctor_gather_state(
|
|
|
1683
2410
|
the TUI and every snapshot build gather on each rebuild, where serial
|
|
1684
2411
|
2-second probes of every live descriptor do not belong.
|
|
1685
2412
|
"""
|
|
2413
|
+
import _cctally_store
|
|
2414
|
+
|
|
2415
|
+
# #1005: entered FIRST, so the cache-maintenance admission below and every
|
|
2416
|
+
# stats open inside the impl share one wait allowance.
|
|
2417
|
+
with _cctally_store.bounded_diagnostic_waits(
|
|
2418
|
+
_DOCTOR_MAINTENANCE_WAIT_ALLOWANCE_S
|
|
2419
|
+
):
|
|
2420
|
+
return _doctor_gather_state_admitted(
|
|
2421
|
+
now_utc=now_utc,
|
|
2422
|
+
runtime_bind=runtime_bind,
|
|
2423
|
+
deep=deep,
|
|
2424
|
+
force_cold_inputs=force_cold_inputs,
|
|
2425
|
+
discover_dashboards=discover_dashboards,
|
|
2426
|
+
)
|
|
2427
|
+
|
|
2428
|
+
|
|
2429
|
+
def _doctor_gather_state_admitted(
|
|
2430
|
+
*, now_utc, runtime_bind, deep, force_cold_inputs, discover_dashboards,
|
|
2431
|
+
):
|
|
2432
|
+
"""The cache-maintenance admission and the gather, inside the scope."""
|
|
2433
|
+
import _cctally_store
|
|
2434
|
+
|
|
1686
2435
|
cache_lock = None
|
|
1687
2436
|
cache_probe_allowed = not _cctally_core.CACHE_DB_PATH.exists()
|
|
1688
2437
|
cache_repair_marker = {
|
|
@@ -1695,8 +2444,18 @@ def doctor_gather_state(
|
|
|
1695
2444
|
lock_path = _cctally_core.CACHE_LOCK_MAINTENANCE_PATH
|
|
1696
2445
|
if lock_path.exists():
|
|
1697
2446
|
cache_lock = open(lock_path, "r")
|
|
1698
|
-
|
|
1699
|
-
|
|
2447
|
+
# #1005 R11: bounded by the gather's allowance instead of a
|
|
2448
|
+
# blocking flock. On decline every cache probe degrades
|
|
2449
|
+
# through the existing "cache maintenance excludes read
|
|
2450
|
+
# probes" path.
|
|
2451
|
+
if _cctally_store.diagnostic_flock(
|
|
2452
|
+
cache_lock, fcntl.LOCK_SH, 5.0
|
|
2453
|
+
):
|
|
2454
|
+
cache_probe_allowed = True
|
|
2455
|
+
else:
|
|
2456
|
+
cache_lock.close()
|
|
2457
|
+
cache_lock = None
|
|
2458
|
+
cache_probe_allowed = False
|
|
1700
2459
|
|
|
1701
2460
|
c = _cctally()
|
|
1702
2461
|
db_mod = c._load_sibling("_cctally_db")
|
|
@@ -1727,8 +2486,6 @@ def doctor_gather_state(
|
|
|
1727
2486
|
cache_lock.close()
|
|
1728
2487
|
cache_lock = None
|
|
1729
2488
|
|
|
1730
|
-
import _cctally_store
|
|
1731
|
-
|
|
1732
2489
|
with _cctally_store.suppress_interrupted_stats_recovery():
|
|
1733
2490
|
return _doctor_gather_state_impl(
|
|
1734
2491
|
now_utc=now_utc,
|
|
@@ -2006,12 +2763,22 @@ def _doctor_gather_state_impl(
|
|
|
2006
2763
|
|
|
2007
2764
|
with _lib_perf.phase("doctor.db_status"):
|
|
2008
2765
|
# ── DB ───────────────────────────────────────────────────────────
|
|
2766
|
+
interrupted_inspection_error = None
|
|
2009
2767
|
try:
|
|
2010
2768
|
import _cctally_store
|
|
2011
2769
|
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2770
|
+
try:
|
|
2771
|
+
interrupted = _cctally_store.stats_interrupted_rebuild_evidence(
|
|
2772
|
+
_cctally_core.DB_PATH
|
|
2773
|
+
)
|
|
2774
|
+
except OSError as exc:
|
|
2775
|
+
# #1005: the lock-free high-water read lost a listing race
|
|
2776
|
+
# twice, or the file system refused it. The evidence is
|
|
2777
|
+
# unknown, never "no rebuild": the report continues, and the
|
|
2778
|
+
# freshness leg refuses to read an absent file as a fresh
|
|
2779
|
+
# install.
|
|
2780
|
+
interrupted = None
|
|
2781
|
+
interrupted_inspection_error = str(exc)
|
|
2015
2782
|
if interrupted is not None and interrupted.get("live") is True:
|
|
2016
2783
|
stats_db_status = {
|
|
2017
2784
|
"path": str(_cctally_core.DB_PATH),
|
|
@@ -2034,6 +2801,10 @@ def _doctor_gather_state_impl(
|
|
|
2034
2801
|
stats_db_status = {"path": str(_cctally_core.DB_PATH), "user_version": 0,
|
|
2035
2802
|
"registry_size": len(c._STATS_MIGRATIONS),
|
|
2036
2803
|
"migrations": [], "_open_error": str(exc)}
|
|
2804
|
+
if interrupted_inspection_error is not None:
|
|
2805
|
+
stats_db_status["_interrupted_inspection_error"] = (
|
|
2806
|
+
interrupted_inspection_error
|
|
2807
|
+
)
|
|
2037
2808
|
# stats.db is the epoch-versioned journal index (DB journal redesign §7.1):
|
|
2038
2809
|
# feed the epoch constant to the pure kernel so db.version_ahead classifies
|
|
2039
2810
|
# uv==1000 as HEALTHY (not a #145 version-ahead FAIL). registry_size stays the
|
|
@@ -2069,9 +2840,12 @@ def _doctor_gather_state_impl(
|
|
|
2069
2840
|
with _lib_perf.phase("doctor.stats_freshness"):
|
|
2070
2841
|
# ── Data freshness ───────────────────────────────────────────────
|
|
2071
2842
|
latest_snapshot_at = None
|
|
2843
|
+
latest_reading_at = None
|
|
2844
|
+
freshness_unavailable_reason = None
|
|
2072
2845
|
forked_bucket_counts: dict | None = None
|
|
2073
2846
|
credited_weeks: list[dict] | None = None
|
|
2074
2847
|
try:
|
|
2848
|
+
conn = None
|
|
2075
2849
|
if _cctally_core.DB_PATH.exists():
|
|
2076
2850
|
# #386 spec section 3.1, third clause: EVERY opener of the live stats
|
|
2077
2851
|
# family participates in the replacement protocol, read-only probes
|
|
@@ -2081,7 +2855,19 @@ def _doctor_gather_state_impl(
|
|
|
2081
2855
|
# such on this project twice. Participation, not read-only-ness, is
|
|
2082
2856
|
# what the clause requires.
|
|
2083
2857
|
import _cctally_store as _store_mod
|
|
2084
|
-
|
|
2858
|
+
try:
|
|
2859
|
+
conn = _store_mod.stats_open_guarded(_cctally_core.DB_PATH)
|
|
2860
|
+
except _store_mod._cctally_db.StatsDbMaintenanceError:
|
|
2861
|
+
# #1018 C6: neither source can be read, which is not
|
|
2862
|
+
# silence; the check reports it as unavailable.
|
|
2863
|
+
freshness_unavailable_reason = "stats_maintenance"
|
|
2864
|
+
except Exception:
|
|
2865
|
+
freshness_unavailable_reason = "stats_unreadable"
|
|
2866
|
+
else:
|
|
2867
|
+
freshness_unavailable_reason = _classify_absent_stats(
|
|
2868
|
+
stats_db_status
|
|
2869
|
+
)
|
|
2870
|
+
if conn is not None:
|
|
2085
2871
|
try:
|
|
2086
2872
|
# HELD-INCLUSIVE, and that is the decision rather than an
|
|
2087
2873
|
# omission (#769 S11, #824). Two different freshness
|
|
@@ -2106,16 +2892,17 @@ def _doctor_gather_state_impl(
|
|
|
2106
2892
|
# Pinned by `test_doctor_latest_snapshot_age_counts_a_held_row`
|
|
2107
2893
|
# so a later change that silently converts this leg to the
|
|
2108
2894
|
# second question fails rather than passing quietly.
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2895
|
+
#
|
|
2896
|
+
# #1018: a snapshot row is written only when a reading
|
|
2897
|
+
# CHANGES value, so flat usage leaves it old while the
|
|
2898
|
+
# pipeline is healthy. The second source, the newest
|
|
2899
|
+
# admitted reading in the observation index, answers the
|
|
2900
|
+
# same pipeline question; the check takes the newer.
|
|
2901
|
+
(
|
|
2902
|
+
latest_snapshot_at,
|
|
2903
|
+
latest_reading_at,
|
|
2904
|
+
freshness_unavailable_reason,
|
|
2905
|
+
) = _gather_freshness_sources(conn, now_utc)
|
|
2119
2906
|
# Forked-bucket invariant probe. Each fork count is
|
|
2120
2907
|
# a raw SELECT against the already-open connection —
|
|
2121
2908
|
# no bonus open_db() recursion. Tables missing →
|
|
@@ -2370,7 +3157,8 @@ def _doctor_gather_state_impl(
|
|
|
2370
3157
|
# Non-blocking flock probe: if a transcript writer/reingest holds the
|
|
2371
3158
|
# conversations.db lock, the rollup may be mid-recompute → in progress. We
|
|
2372
3159
|
# acquire LOCK_EX|LOCK_NB and immediately release; failure (held) is the
|
|
2373
|
-
# signal. Never blocks (LOCK_NB), so doctor stays
|
|
3160
|
+
# signal. Never blocks (LOCK_NB), so doctor stays prompt and never
|
|
3161
|
+
# changes your data.
|
|
2374
3162
|
if not conv_rollup_sync_in_progress:
|
|
2375
3163
|
lock_path = _cctally_core.CONVERSATIONS_LOCK_PATH
|
|
2376
3164
|
if lock_path is not None and pathlib.Path(lock_path).exists():
|
|
@@ -2794,7 +3582,7 @@ def _doctor_gather_state_impl(
|
|
|
2794
3582
|
#
|
|
2795
3583
|
# Raw JSON read (NOT load_config or _load_config_unlocked): both
|
|
2796
3584
|
# call `ensure_dirs()`, which creates `~/.local/share/cctally/`
|
|
2797
|
-
# and `logs/` on a fresh HOME. Doctor
|
|
3585
|
+
# and `logs/` on a fresh HOME. Doctor never changes your data
|
|
2798
3586
|
# (H1 invariant) — it must never mutate user state, even by
|
|
2799
3587
|
# creating an empty directory tree. Corrupt JSON yields
|
|
2800
3588
|
# `dashboard_bind_stored = "loopback"` (the same fallback the
|
|
@@ -2936,11 +3724,12 @@ def _doctor_gather_state_impl(
|
|
|
2936
3724
|
# ── Meta ─────────────────────────────────────────────────────────
|
|
2937
3725
|
with _lib_perf.phase("doctor.journal"):
|
|
2938
3726
|
# ── Journal (DB journal redesign §9) ─────────────────────────────
|
|
2939
|
-
#
|
|
2940
|
-
#
|
|
2941
|
-
#
|
|
2942
|
-
#
|
|
2943
|
-
#
|
|
3727
|
+
# Legs over the append-only journal, read-only except the deep scan's
|
|
3728
|
+
# disposable checkpoint (journal/.doctor-scan-index, #1054): presence +
|
|
3729
|
+
# appendability, torn-tail/malformed counts (deep-gated — reads whole
|
|
3730
|
+
# segments), the ingest cursor lag vs. the high-water, and the auto-heal
|
|
3731
|
+
# incident history. Every probe degrades to its always-OK posture on any
|
|
3732
|
+
# error (the pure kernel then reports "no journal" / "not scanned").
|
|
2944
3733
|
import _lib_journal as _jl
|
|
2945
3734
|
journal_present = False
|
|
2946
3735
|
journal_appendable = None
|
|
@@ -3022,9 +3811,6 @@ def _doctor_gather_state_impl(
|
|
|
3022
3811
|
if deep and segs:
|
|
3023
3812
|
malformed = 0
|
|
3024
3813
|
torn = 0
|
|
3025
|
-
decoded_records: list = []
|
|
3026
|
-
protocol_evidence = []
|
|
3027
|
-
protocol_evidence_owners = []
|
|
3028
3814
|
prior_high_water = None
|
|
3029
3815
|
cutover_value = None
|
|
3030
3816
|
# #825/#832 (spec 5.3, 8): the deep scan applies the same
|
|
@@ -3032,10 +3818,24 @@ def _doctor_gather_state_impl(
|
|
|
3032
3818
|
# within this one pass and applied after it — and
|
|
3033
3819
|
# recomputes the excluded population the legacy-writer leg
|
|
3034
3820
|
# compares.
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3821
|
+
scan_state = _DeepScanState()
|
|
3822
|
+
pass_boundary = scan_state.pass_boundary
|
|
3823
|
+
decoded_records = scan_state.decoded_records
|
|
3824
|
+
deep_coordinates = scan_state.deep_coordinates
|
|
3825
|
+
protocol_evidence = scan_state.protocol_evidence
|
|
3826
|
+
protocol_evidence_owners = (
|
|
3827
|
+
scan_state.protocol_evidence_owners)
|
|
3828
|
+
cutover_candidates = scan_state.cutover_candidates
|
|
3038
3829
|
deep_excluded: list = []
|
|
3830
|
+
# #1054: the doctor-written checkpoint. Every entry is
|
|
3831
|
+
# proven against the bytes read below before any of it is
|
|
3832
|
+
# used, and the new one covers exactly the segments read.
|
|
3833
|
+
import _lib_doctor_scan_index as _dsi
|
|
3834
|
+
scan_version = _dsi.index_version(
|
|
3835
|
+
_deep_scan_index_descriptors())
|
|
3836
|
+
scan_index, scan_index_clean = _load_deep_scan_index(
|
|
3837
|
+
jdir, scan_version)
|
|
3838
|
+
scan_entries: list = []
|
|
3039
3839
|
for seg in segs:
|
|
3040
3840
|
try:
|
|
3041
3841
|
data = (jdir / seg).read_bytes()
|
|
@@ -3047,59 +3847,16 @@ def _doctor_gather_state_impl(
|
|
|
3047
3847
|
journal_scan_error = _scan_cause(exc)
|
|
3048
3848
|
break
|
|
3049
3849
|
if not data:
|
|
3850
|
+
# Spec 5.3: an empty segment is skipped — no
|
|
3851
|
+
# entry, and the prior high-water is untouched.
|
|
3050
3852
|
continue
|
|
3051
3853
|
if not data.endswith(b"\n"):
|
|
3052
3854
|
torn += 1
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
if not raw:
|
|
3059
|
-
prior_high_water = (seg, offset + 1)
|
|
3060
|
-
offset += 1
|
|
3061
|
-
continue
|
|
3062
|
-
record = _jl.decode_line(raw)
|
|
3063
|
-
if record is None:
|
|
3064
|
-
malformed += 1
|
|
3065
|
-
prior_high_water = (
|
|
3066
|
-
seg,
|
|
3067
|
-
offset + len(raw) + 1,
|
|
3068
|
-
)
|
|
3069
|
-
offset += len(raw) + 1
|
|
3070
|
-
continue
|
|
3071
|
-
pass_boundary.observe(record)
|
|
3072
|
-
_jr._capture_protocol_prefix_evidence(
|
|
3073
|
-
record,
|
|
3074
|
-
prior_high_water,
|
|
3075
|
-
protocol_evidence,
|
|
3076
|
-
)
|
|
3077
|
-
if (len(protocol_evidence)
|
|
3078
|
-
> len(protocol_evidence_owners)):
|
|
3079
|
-
protocol_evidence_owners.append(
|
|
3080
|
-
len(decoded_records))
|
|
3081
|
-
# first cutover op wins, exactly as
|
|
3082
|
-
# `find_accounts_cutover_op` scans — captured here so the
|
|
3083
|
-
# conflict scan does not decode the whole journal twice.
|
|
3084
|
-
if record.get("id") == _jr.CUTOVER_OP_ID:
|
|
3085
|
-
cutover_candidates.append(
|
|
3086
|
-
(record, seg, offset))
|
|
3087
|
-
# RETAIN ONLY what the selector consumes. `obs` lines are
|
|
3088
|
-
# ~97% of a real journal (984k of 1.02M) and
|
|
3089
|
-
# `resolve_effective_events` ignores them entirely —
|
|
3090
|
-
# keeping their dictionaries cost 4.3 GB of peak RSS for
|
|
3091
|
-
# an identical result (#374 review). They still consume a
|
|
3092
|
-
# lightweight slot because their physical sequence is
|
|
3093
|
-
# part of three durable violation fingerprints (#508).
|
|
3094
|
-
decoded_records.append(
|
|
3095
|
-
_lib_journal_router.selector_slot(record)
|
|
3096
|
-
)
|
|
3097
|
-
deep_coordinates.append((seg, offset))
|
|
3098
|
-
prior_high_water = (
|
|
3099
|
-
seg,
|
|
3100
|
-
offset + len(raw) + 1,
|
|
3101
|
-
)
|
|
3102
|
-
offset += len(raw) + 1
|
|
3855
|
+
seg_entry, seg_malformed, prior_high_water = (
|
|
3856
|
+
_deep_scan_segment(seg, data, scan_index.get(seg),
|
|
3857
|
+
prior_high_water, scan_state))
|
|
3858
|
+
malformed += seg_malformed
|
|
3859
|
+
scan_entries.append(seg_entry)
|
|
3103
3860
|
admission_boundary = None
|
|
3104
3861
|
if journal_scan_error is None:
|
|
3105
3862
|
try:
|
|
@@ -3109,10 +3866,12 @@ def _doctor_gather_state_impl(
|
|
|
3109
3866
|
# K9K10-R6: a None boundary admits everything:
|
|
3110
3867
|
# zero excluded records and an unfiltered scan.
|
|
3111
3868
|
journal_scan_error = _scan_cause(exc)
|
|
3869
|
+
exclusion_contributions: dict = {}
|
|
3112
3870
|
if journal_scan_error is None:
|
|
3113
3871
|
try:
|
|
3114
|
-
deep_excluded =
|
|
3115
|
-
admission_boundary
|
|
3872
|
+
deep_excluded = _deep_excluded_population(
|
|
3873
|
+
admission_boundary, scan_index,
|
|
3874
|
+
exclusion_contributions)
|
|
3116
3875
|
except Exception as exc: # noqa: BLE001
|
|
3117
3876
|
# K9K10-R11: an unreadable tail is never a
|
|
3118
3877
|
# smaller population.
|
|
@@ -3124,6 +3883,17 @@ def _doctor_gather_state_impl(
|
|
|
3124
3883
|
else:
|
|
3125
3884
|
journal_malformed_count = malformed
|
|
3126
3885
|
journal_torn_tail_count = torn
|
|
3886
|
+
# An exclusion contribution rides on its segment's
|
|
3887
|
+
# main entry; a segment the main pass did not read
|
|
3888
|
+
# gets no entry at all.
|
|
3889
|
+
_publish_deep_scan_index(jdir, scan_version, [
|
|
3890
|
+
_dsi.Entry(entry.name, entry.covered,
|
|
3891
|
+
entry.sha256, entry.decoded,
|
|
3892
|
+
entry.malformed, entry.hits,
|
|
3893
|
+
exclusion_contributions.get(
|
|
3894
|
+
entry.name))
|
|
3895
|
+
for entry in scan_entries
|
|
3896
|
+
], scan_index, scan_index_clean)
|
|
3127
3897
|
if deep and segs and journal_scan_error is None:
|
|
3128
3898
|
_jr._apply_pass_admission(
|
|
3129
3899
|
decoded_records, deep_coordinates, admission_boundary)
|
|
@@ -3381,6 +4151,8 @@ def _doctor_gather_state_impl(
|
|
|
3381
4151
|
stats_db_status=stats_db_status,
|
|
3382
4152
|
cache_db_status=cache_db_status,
|
|
3383
4153
|
latest_snapshot_at=latest_snapshot_at,
|
|
4154
|
+
latest_reading_at=latest_reading_at,
|
|
4155
|
+
freshness_unavailable_reason=freshness_unavailable_reason,
|
|
3384
4156
|
cache_entries_count=cache_entries_count,
|
|
3385
4157
|
cache_last_entry_at=cache_last_entry_at,
|
|
3386
4158
|
claude_jsonl_present=claude_jsonl_present,
|