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.
Files changed (58) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +7 -3
  3. package/bin/_cctally_cache.py +2783 -175
  4. package/bin/_cctally_completion.py +106 -33
  5. package/bin/_cctally_core.py +69 -4
  6. package/bin/_cctally_dashboard.py +188 -13
  7. package/bin/_cctally_dashboard_conversation.py +5 -1
  8. package/bin/_cctally_dashboard_envelope.py +50 -19
  9. package/bin/_cctally_dashboard_sources.py +23 -1
  10. package/bin/_cctally_db.py +193 -153
  11. package/bin/_cctally_diagnosis_sources.py +63 -7
  12. package/bin/_cctally_doctor.py +865 -93
  13. package/bin/_cctally_five_hour.py +67 -50
  14. package/bin/_cctally_forecast.py +35 -13
  15. package/bin/_cctally_journal.py +186 -18
  16. package/bin/_cctally_milestone_history.py +859 -62
  17. package/bin/_cctally_oauth_samples.py +286 -15
  18. package/bin/_cctally_parser.py +2 -2
  19. package/bin/_cctally_quota.py +104 -12
  20. package/bin/_cctally_record.py +153 -29
  21. package/bin/_cctally_rederive.py +608 -68
  22. package/bin/_cctally_statusline.py +108 -81
  23. package/bin/_cctally_store.py +179 -38
  24. package/bin/_cctally_tui.py +892 -181
  25. package/bin/_cctally_update.py +32 -13
  26. package/bin/_lib_codex_conversation_query.py +52 -14
  27. package/bin/_lib_codex_drain_admission.py +224 -6
  28. package/bin/_lib_codex_metadata.py +39 -0
  29. package/bin/_lib_conversation_anon.py +15 -8
  30. package/bin/_lib_conversation_dispatch.py +7 -0
  31. package/bin/_lib_conversation_query.py +289 -70
  32. package/bin/_lib_dashboard_sources.py +37 -1
  33. package/bin/_lib_detector_access_registry.py +497 -26
  34. package/bin/_lib_doctor.py +85 -20
  35. package/bin/_lib_doctor_scan_index.py +321 -0
  36. package/bin/_lib_ingest_frontier.py +20 -1
  37. package/bin/_lib_oauth_samples.py +23 -7
  38. package/bin/_lib_pricing.py +146 -16
  39. package/bin/_lib_quota.py +19 -0
  40. package/bin/_lib_rederive.py +11 -6
  41. package/bin/_lib_retained_records.py +516 -0
  42. package/bin/_lib_snapshot_cache.py +10 -0
  43. package/bin/_lib_stale_proof.py +272 -43
  44. package/bin/_lib_stats_content_revision.py +831 -0
  45. package/bin/_lib_write_io.py +11 -4
  46. package/bin/cctally +4 -1
  47. package/dashboard/static/assets/ConversationsView-D4Iv4ku0.js +72 -0
  48. package/dashboard/static/assets/{DoctorModal-B0DnU9d9.js → DoctorModal-D4QSq6uz.js} +1 -1
  49. package/dashboard/static/assets/ModalRoot-C9F5dhZi.js +1 -0
  50. package/dashboard/static/assets/{ProjectsDrillPanel-DSxXQSa-.js → ProjectsDrillPanel-WxAdRDMt.js} +1 -1
  51. package/dashboard/static/assets/{SourceDetailModal-BleDpQqG.js → SourceDetailModal-NyyEfXmc.js} +1 -1
  52. package/dashboard/static/assets/{UpdateModal-CMqaAkSK.js → UpdateModal-CFLq9pT1.js} +1 -1
  53. package/dashboard/static/assets/{index-BjQV-0-D.js → index-BKTWH9JE.js} +2 -2
  54. package/dashboard/static/assets/{outlineNavigation-CO-VWBm7.js → outlineNavigation-BnGUwCS3.js} +3 -3
  55. package/dashboard/static/dashboard.html +1 -1
  56. package/package.json +4 -1
  57. package/dashboard/static/assets/ConversationsView-oqNsWzWV.js +0 -72
  58. package/dashboard/static/assets/ModalRoot-CYua2wP_.js +0 -1
@@ -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 is read-only. Discovery runs only for the
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 read-only health report, not an
942
- #: ingest, and the state it would have reported self-heals on the next sync.
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
- high_water = _cctally_journal.journal_high_water()
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 is read-only, so it never creates the maintenance lock. When the
1671
- lock already exists it holds maintenance-shared from the marker check
1672
- through every cache probe. A live/stale marker or a pending quarantine
1673
- suppresses all raw SQLite cache opens; when a cache exists but its lock is
1674
- absent, probes also degrade rather than racing a newly starting repair.
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
- fcntl.flock(cache_lock, fcntl.LOCK_SH)
1699
- cache_probe_allowed = True
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
- interrupted = _cctally_store.stats_interrupted_rebuild_evidence(
2013
- _cctally_core.DB_PATH
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
- conn = _store_mod.stats_open_guarded(_cctally_core.DB_PATH)
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
- try:
2110
- row = conn.execute(
2111
- "SELECT MAX(captured_at_utc) FROM weekly_usage_snapshots"
2112
- ).fetchone()
2113
- if row and row[0]:
2114
- latest_snapshot_at = parse_iso_datetime(
2115
- row[0], "weekly_usage_snapshots.captured_at_utc",
2116
- ).astimezone(dt.timezone.utc)
2117
- except sqlite3.OperationalError:
2118
- pass # table missing — treat as no snapshots yet
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 read-only + prompt.
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 is a read-only diagnostic
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
- # Read-only legs over the append-only journal: presence + appendability,
2940
- # torn-tail/malformed counts (deep-gated — reads whole segments), the ingest
2941
- # cursor lag vs. the high-water, and the auto-heal incident history. Every
2942
- # probe degrades to its always-OK posture on any error (the pure kernel then
2943
- # reports "no journal" / "not scanned").
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
- pass_boundary = _jr._PassBoundary()
3036
- deep_coordinates: list = []
3037
- cutover_candidates: list = []
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
- # every element except the last is a complete line; the last
3054
- # is either "" (ended in \n) or the torn partial — not a
3055
- # mid-file line, so it is never counted as malformed.
3056
- offset = 0
3057
- for raw in data.split(b"\n")[:-1]:
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 = _jr.excluded_population(
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,