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
@@ -0,0 +1,516 @@
1
+ """Coordinate-backed retained journal records (#1004, spec D3 and D4).
2
+
3
+ A journal reader that keeps every record it decoded until its planning or
4
+ replay passes end holds ≈ 0.9 KB per record, plus two lists of
5
+ ``(segment, offset)`` tuples at ≈ 100 bytes per record each. This module keeps
6
+ coordinates instead and re-reads a record's line when a consumer asks for it:
7
+
8
+ * `LineCoordinates` holds one ``array('Q')`` of line start offsets and one
9
+ ``array('I')`` of line lengths (the newline excluded), 12 bytes a position,
10
+ plus a table of segment runs. Its ``starts()`` and ``ends()`` views yield
11
+ the ``(segment, start)`` and ``(segment, start + length + 1)`` tuples the
12
+ readers' two lists held.
13
+ * `RetainedRecords` is a sequence over those positions. A slot holds either a
14
+ retained object (an interned stub, an admission placeholder, a ``None``
15
+ slot) or the private `STREAMED` sentinel, which means "decode from the
16
+ coordinates". A streamed line's 8-byte BLAKE2b digest is taken in the read
17
+ pass and checked on every re-read, so a rewrite of the same length, which
18
+ framing alone cannot detect, raises too.
19
+
20
+ Pure and stdlib-only: the caller supplies the line reader, the decoder and the
21
+ error type, so the readers keep their own framing check and error contract.
22
+ """
23
+ from __future__ import annotations
24
+
25
+ import bisect
26
+ import hashlib
27
+ from array import array
28
+ from collections.abc import Sequence
29
+ from typing import Callable, Protocol
30
+
31
+ #: Test switch (#1004 T6): when true, the readers build today's list form (a
32
+ #: plain list of decoded records and a plain list of end tuples) instead of a
33
+ #: `RetainedRecords`. Read at reader time. Never set by product code.
34
+ MATERIALIZE_FOR_TESTS = False
35
+
36
+
37
+ class _Streamed:
38
+ """The slot marker for "decode this position from its coordinates"."""
39
+
40
+ __slots__ = ()
41
+
42
+ def __repr__(self) -> str:
43
+ return "STREAMED"
44
+
45
+
46
+ #: The one slot value meaning a position is decoded on access.
47
+ STREAMED = _Streamed()
48
+
49
+
50
+ def line_digest(line: bytes) -> int:
51
+ """The 8-byte BLAKE2b digest of ``line``, as an unsigned integer."""
52
+ return int.from_bytes(
53
+ hashlib.blake2b(line, digest_size=8).digest(), "little")
54
+
55
+
56
+ def _sequence_eq(left, right):
57
+ if not isinstance(right, (list, tuple, Sequence)) or isinstance(
58
+ right, (str, bytes)):
59
+ return NotImplemented
60
+ if len(left) != len(right):
61
+ return False
62
+ return all(a == b for a, b in zip(left, right))
63
+
64
+
65
+ # --------------------------------------------------------------------------
66
+ # D3: per-position coordinates
67
+ # --------------------------------------------------------------------------
68
+
69
+
70
+ class LineCoordinates:
71
+ """Per-position line coordinates in two arrays and a segment-run table.
72
+
73
+ ``append(segment, start, length)`` adds one position: ``start`` is the
74
+ line's first byte and ``length`` excludes its newline. Positions are in
75
+ journal order, so within one run the end coordinates strictly increase.
76
+ """
77
+
78
+ __slots__ = ("_starts", "_lengths", "_run_segments", "_run_firsts",
79
+ "__weakref__")
80
+
81
+ def __init__(self) -> None:
82
+ self._starts = array("Q")
83
+ self._lengths = array("I")
84
+ #: One entry per run of consecutive positions in the same segment.
85
+ self._run_segments: list = []
86
+ self._run_firsts = array("Q")
87
+
88
+ def append(self, segment: str, start: int, length: int) -> None:
89
+ segments = self._run_segments
90
+ if not segments or segments[-1] != segment:
91
+ segments.append(segment)
92
+ self._run_firsts.append(len(self._starts))
93
+ self._starts.append(start)
94
+ self._lengths.append(length)
95
+
96
+ def __len__(self) -> int:
97
+ return len(self._starts)
98
+
99
+ def _run_of(self, index: int) -> int:
100
+ return bisect.bisect_right(self._run_firsts, index) - 1
101
+
102
+ def _normalize(self, index: int) -> int:
103
+ size = len(self._starts)
104
+ if index < 0:
105
+ index += size
106
+ if not 0 <= index < size:
107
+ raise IndexError("line coordinate index out of range")
108
+ return index
109
+
110
+ def line(self, i: int) -> "tuple[str, int, int]":
111
+ """``(segment, start, length)`` of position ``i``."""
112
+ i = self._normalize(i)
113
+ return (self._run_segments[self._run_of(i)], self._starts[i],
114
+ self._lengths[i])
115
+
116
+ def _start(self, i: int):
117
+ i = self._normalize(i)
118
+ return (self._run_segments[self._run_of(i)], self._starts[i])
119
+
120
+ def _end(self, i: int):
121
+ i = self._normalize(i)
122
+ return (self._run_segments[self._run_of(i)],
123
+ self._starts[i] + self._lengths[i] + 1)
124
+
125
+ def _iter(self, positions, end: bool):
126
+ starts = self._starts
127
+ lengths = self._lengths
128
+ segments = self._run_segments
129
+ firsts = self._run_firsts
130
+ run = -1
131
+ run_stop = -1
132
+ segment = None
133
+ for i in positions:
134
+ if not (run >= 0 and firsts[run] <= i < run_stop):
135
+ run = bisect.bisect_right(firsts, i) - 1
136
+ segment = segments[run]
137
+ run_stop = (firsts[run + 1] if run + 1 < len(firsts)
138
+ else len(starts))
139
+ if end:
140
+ yield (segment, starts[i] + lengths[i] + 1)
141
+ else:
142
+ yield (segment, starts[i])
143
+
144
+ def _index_end(self, cursor) -> int:
145
+ """The position whose end coordinate is ``cursor``; ValueError when
146
+ none is. A binary search inside each run of ``cursor``'s segment."""
147
+ try:
148
+ segment, end = cursor
149
+ except (TypeError, ValueError):
150
+ raise ValueError(f"{cursor!r} is not in list") from None
151
+ starts = self._starts
152
+ lengths = self._lengths
153
+ firsts = self._run_firsts
154
+ for run, name in enumerate(self._run_segments):
155
+ if name != segment:
156
+ continue
157
+ lo = firsts[run]
158
+ hi = firsts[run + 1] if run + 1 < len(firsts) else len(starts)
159
+ while lo < hi:
160
+ mid = (lo + hi) // 2
161
+ value = starts[mid] + lengths[mid] + 1
162
+ if value < end:
163
+ lo = mid + 1
164
+ elif value > end:
165
+ hi = mid
166
+ else:
167
+ return mid
168
+ raise ValueError(f"{cursor!r} is not in list")
169
+
170
+ def starts(self) -> "_CoordinateView":
171
+ """Every position's ``(segment, start)``, as a view."""
172
+ return _CoordinateView(self, None, end=False)
173
+
174
+ def ends(self) -> "_CoordinateView":
175
+ """Every position's ``(segment, start + length + 1)``, as a view."""
176
+ return _CoordinateView(self, None, end=True)
177
+
178
+
179
+ class _CoordinateView(Sequence):
180
+ """A view of `LineCoordinates` positions as start or end tuples.
181
+
182
+ ``positions`` is ``None`` for the whole (live) coordinate set, otherwise a
183
+ ``range`` of positions. Slicing returns a view; nothing is copied."""
184
+
185
+ __slots__ = ("_coords", "_positions", "_end", "__weakref__")
186
+
187
+ def __init__(self, coords: LineCoordinates, positions, *, end: bool):
188
+ self._coords = coords
189
+ self._positions = positions
190
+ self._end = end
191
+
192
+ def _range(self):
193
+ if self._positions is None:
194
+ return range(len(self._coords))
195
+ return self._positions
196
+
197
+ def __len__(self) -> int:
198
+ return len(self._range())
199
+
200
+ def __getitem__(self, index):
201
+ positions = self._range()
202
+ if isinstance(index, slice):
203
+ return _CoordinateView(self._coords, positions[index],
204
+ end=self._end)
205
+ position = positions[index]
206
+ return (self._coords._end(position) if self._end
207
+ else self._coords._start(position))
208
+
209
+ def __iter__(self):
210
+ return self._coords._iter(self._range(), self._end)
211
+
212
+ def index(self, cursor, start=0, stop=None):
213
+ if not self._end:
214
+ return Sequence.index(self, cursor, start, stop)
215
+ position = self._coords._index_end(cursor)
216
+ positions = self._range()
217
+ try:
218
+ found = positions.index(position)
219
+ except ValueError:
220
+ raise ValueError(f"{cursor!r} is not in list") from None
221
+ if stop is None:
222
+ stop = len(positions)
223
+ if start < 0:
224
+ start = max(len(positions) + start, 0)
225
+ if stop < 0:
226
+ stop += len(positions)
227
+ if not start <= found < stop:
228
+ raise ValueError(f"{cursor!r} is not in list")
229
+ return found
230
+
231
+ def __eq__(self, other):
232
+ return _sequence_eq(self, other)
233
+
234
+ __hash__ = None # type: ignore[assignment]
235
+
236
+ def __repr__(self) -> str:
237
+ kind = "ends" if self._end else "starts"
238
+ return f"<LineCoordinates.{kind} view of {len(self)}>"
239
+
240
+
241
+ # --------------------------------------------------------------------------
242
+ # D4: coordinate-backed records
243
+ # --------------------------------------------------------------------------
244
+
245
+
246
+ class LineReader(Protocol):
247
+ """Reads one journal line back by its coordinates."""
248
+
249
+ def read(self, segment: str, start: int, length: int) -> bytes:
250
+ """The ``length`` bytes at ``start``, verified to still be one framed
251
+ line; raises the caller's error on a framing mismatch."""
252
+
253
+ def close(self) -> None:
254
+ """Release every handle."""
255
+
256
+
257
+ class RetainedRecords(Sequence):
258
+ """A reader's retained records, streamed from coordinates (spec D4).
259
+
260
+ Each position's slot holds either a retained object or `STREAMED`. A
261
+ streamed position is re-read on access through a reader from
262
+ ``open_reader``, digest-checked, decoded with ``decode`` and normalized in
263
+ place by the installed normalizer: each access returns a fresh private
264
+ object equal to the one the reader retained before. ``error(message)``
265
+ builds the exception a changed line raises."""
266
+
267
+ def __init__(self, coordinates: LineCoordinates,
268
+ open_reader: "Callable[[], LineReader]",
269
+ decode: "Callable[[bytes], dict]", *,
270
+ error: "Callable[[str], Exception]") -> None:
271
+ self._coords = coordinates
272
+ self._open_reader = open_reader
273
+ self._decode = decode
274
+ self._error = error
275
+ self._slots: list = []
276
+ #: One digest per position (zero for a retained slot).
277
+ self._digests = array("Q")
278
+ self._normalizer = None
279
+ self._reader = None
280
+
281
+ # -- building -----------------------------------------------------------
282
+
283
+ def append_retained(self, obj, segment: str, start: int,
284
+ length: int) -> None:
285
+ self._coords.append(segment, start, length)
286
+ self._slots.append(obj)
287
+ self._digests.append(0)
288
+
289
+ def append_streamed(self, line: bytes, segment: str, start: int,
290
+ length: int) -> None:
291
+ self._coords.append(segment, start, length)
292
+ self._slots.append(STREAMED)
293
+ self._digests.append(line_digest(line))
294
+
295
+ def set_normalizer(self, fn: "Callable[[dict], None] | None") -> None:
296
+ """Install ``fn``, applied in place to every fresh decode."""
297
+ self._normalizer = fn
298
+
299
+ @property
300
+ def coordinates(self) -> LineCoordinates:
301
+ return self._coords
302
+
303
+ # -- reading ------------------------------------------------------------
304
+
305
+ def _decode_at(self, reader, index: int):
306
+ segment, start, length = self._coords.line(index)
307
+ line = reader.read(segment, start, length)
308
+ if line_digest(line) != self._digests[index]:
309
+ raise self._error(
310
+ f"{segment}@{start}: the line the read pass saw has changed")
311
+ record = self._decode(line)
312
+ if record is None:
313
+ raise self._error(
314
+ f"{segment}@{start}: the line the read pass saw no longer "
315
+ "decodes")
316
+ if self._normalizer is not None:
317
+ self._normalizer(record)
318
+ return record
319
+
320
+ def _random_reader(self):
321
+ if self._reader is None:
322
+ self._reader = self._open_reader()
323
+ return self._reader
324
+
325
+ def _item(self, index: int):
326
+ slot = self._slots[index]
327
+ if slot is STREAMED:
328
+ return self._decode_at(self._random_reader(), index)
329
+ return slot
330
+
331
+ def _iter_positions(self, positions, marked: bool = False):
332
+ """The records at ``positions`` (ascending, for one handle per
333
+ segment), through one reader opened for this pass. ``marked`` yields
334
+ ``(fresh, record)`` pairs, ``fresh`` true for a streamed decode."""
335
+ slots = self._slots
336
+ reader = None
337
+ try:
338
+ for index in positions:
339
+ slot = slots[index]
340
+ if slot is STREAMED:
341
+ if reader is None:
342
+ reader = self._open_reader()
343
+ record = self._decode_at(reader, index)
344
+ yield (True, record) if marked else record
345
+ else:
346
+ yield (False, slot) if marked else slot
347
+ finally:
348
+ if reader is not None:
349
+ reader.close()
350
+
351
+ def iter_marked(self):
352
+ """Every position as ``(fresh, record)``: ``fresh`` is true when the
353
+ record is a private decode of a streamed slot, false for a retained
354
+ object (which a consumer must not mutate)."""
355
+ return self._iter_positions(range(len(self._slots)), marked=True)
356
+
357
+ def __len__(self) -> int:
358
+ return len(self._slots)
359
+
360
+ def __getitem__(self, index):
361
+ if isinstance(index, slice):
362
+ return _RecordsView(self, range(len(self._slots))[index], ())
363
+ size = len(self._slots)
364
+ if index < 0:
365
+ index += size
366
+ if not 0 <= index < size:
367
+ raise IndexError("retained record index out of range")
368
+ return self._item(index)
369
+
370
+ def __iter__(self):
371
+ return self._iter_positions(range(len(self._slots)))
372
+
373
+ def __setitem__(self, index: int, placeholder) -> None:
374
+ # The digest stays: a `slot_snapshot` shares the digest array and
375
+ # still decodes the original at this position.
376
+ self._slots[index] = placeholder
377
+
378
+ def is_streamed(self, i: int) -> bool:
379
+ return self._slots[i] is STREAMED
380
+
381
+ def slot(self, i: int) -> object:
382
+ """The raw slot: the retained object, or `STREAMED`."""
383
+ return self._slots[i]
384
+
385
+ def subset(self, positions: "Sequence[int]") -> "Sequence[dict]":
386
+ return _RecordsView(self, array("Q", positions), ())
387
+
388
+ def with_extra(self, obj) -> "Sequence[dict]":
389
+ return _RecordsView(self, range(len(self._slots)), (obj,))
390
+
391
+ def slot_snapshot(self) -> "RetainedRecords":
392
+ """A copy of the slot list sharing the coordinates and digests."""
393
+ copy = RetainedRecords(self._coords, self._open_reader, self._decode,
394
+ error=self._error)
395
+ copy._slots = list(self._slots)
396
+ copy._digests = self._digests
397
+ copy._normalizer = self._normalizer
398
+ return copy
399
+
400
+ def close(self) -> None:
401
+ """Release the random-access reader's handles. Iteration passes
402
+ release their own when they end."""
403
+ reader, self._reader = self._reader, None
404
+ if reader is not None:
405
+ reader.close()
406
+
407
+ def __del__(self) -> None:
408
+ # A sequence dropped without `close()` (a preview that is simply
409
+ # discarded) still releases its handle at once.
410
+ try:
411
+ self.close()
412
+ except Exception: # noqa: BLE001 - never raise from a finalizer
413
+ pass
414
+
415
+ def __eq__(self, other):
416
+ return _sequence_eq(self, other)
417
+
418
+ __hash__ = None # type: ignore[assignment]
419
+
420
+ def __repr__(self) -> str:
421
+ return f"<RetainedRecords of {len(self._slots)}>"
422
+
423
+
424
+ class _RecordsView(Sequence):
425
+ """Positions of one `RetainedRecords`, followed by retained extras: a
426
+ slice, a position subset, or the sequence with one object appended."""
427
+
428
+ __slots__ = ("_base", "_positions", "_extras", "__weakref__")
429
+
430
+ def __init__(self, base: RetainedRecords, positions, extras: tuple):
431
+ self._base = base
432
+ self._positions = positions
433
+ self._extras = extras
434
+
435
+ def __len__(self) -> int:
436
+ return len(self._positions) + len(self._extras)
437
+
438
+ def _locate(self, index: int):
439
+ """``(True, base position)`` or ``(False, extra index)``."""
440
+ size = len(self)
441
+ if index < 0:
442
+ index += size
443
+ if not 0 <= index < size:
444
+ raise IndexError("retained record index out of range")
445
+ count = len(self._positions)
446
+ if index < count:
447
+ return True, self._positions[index]
448
+ return False, index - count
449
+
450
+ def __getitem__(self, index):
451
+ if isinstance(index, slice):
452
+ return self._slice(index)
453
+ in_base, where = self._locate(index)
454
+ if in_base:
455
+ return self._base._item(where)
456
+ return self._extras[where]
457
+
458
+ def _slice(self, index: slice):
459
+ selected = range(len(self))[index]
460
+ count = len(self._positions)
461
+ step = selected.step
462
+ if step < 0:
463
+ return [self[i] for i in selected]
464
+ start, stop = selected.start, selected.stop
465
+ if start < count:
466
+ base_part = self._positions[start:min(stop, count):step]
467
+ first_extra = start + -(-(count - start) // step) * step
468
+ else:
469
+ base_part = self._positions[0:0]
470
+ first_extra = start
471
+ extras = self._extras[first_extra - count:max(stop - count, 0):step]
472
+ return _RecordsView(self._base, base_part, tuple(extras))
473
+
474
+ def __iter__(self):
475
+ yield from self._base._iter_positions(self._positions)
476
+ yield from self._extras
477
+
478
+ def iter_marked(self):
479
+ yield from self._base._iter_positions(self._positions, marked=True)
480
+ for extra in self._extras:
481
+ yield False, extra
482
+
483
+ def is_streamed(self, i: int) -> bool:
484
+ in_base, where = self._locate(i)
485
+ return in_base and self._base.is_streamed(where)
486
+
487
+ def slot(self, i: int) -> object:
488
+ in_base, where = self._locate(i)
489
+ return self._base.slot(where) if in_base else self._extras[where]
490
+
491
+ def subset(self, positions: "Sequence[int]") -> "Sequence[dict]":
492
+ count = len(self._positions)
493
+ base_part = array("Q")
494
+ extras = []
495
+ for i in positions:
496
+ if i < 0 or i >= len(self):
497
+ raise IndexError("retained record index out of range")
498
+ if i < count:
499
+ if extras:
500
+ return [self[j] for j in positions]
501
+ base_part.append(self._positions[i])
502
+ else:
503
+ extras.append(self._extras[i - count])
504
+ return _RecordsView(self._base, base_part, tuple(extras))
505
+
506
+ def with_extra(self, obj) -> "Sequence[dict]":
507
+ return _RecordsView(self._base, self._positions,
508
+ (*self._extras, obj))
509
+
510
+ def __eq__(self, other):
511
+ return _sequence_eq(self, other)
512
+
513
+ __hash__ = None # type: ignore[assignment]
514
+
515
+ def __repr__(self) -> str:
516
+ return f"<RetainedRecords view of {len(self)}>"
@@ -137,6 +137,14 @@ class SnapshotSignature(NamedTuple):
137
137
  # referenced snapshot moves no leg above. Empty on a store with no retired
138
138
  # snapshot and no unavailable five-hour join, so it is byte-neutral there.
139
139
  claude_weekly_retired_digest: str = ""
140
+ # #1050 spec 4.5 item 1: the published leg of stats.db's Claude content
141
+ # revision (`_lib_stats_content_revision.revision_leg`). An in-place
142
+ # rewrite of a Claude-published row (a Model-A convergence, a reviewed
143
+ # rederive, the open-block projection) moves none of the append-shaped
144
+ # legs above; the trigger-maintained revision moves on every one. `""` on a
145
+ # never-mutated, never-repaired, never-published store (byte-neutral), and
146
+ # a fresh `"?"` nonce per read on an untrusted one, so it never idles.
147
+ claude_content_revision: str = ""
140
148
 
141
149
 
142
150
  def _max_id(conn: sqlite3.Connection, table: str) -> int:
@@ -254,6 +262,7 @@ def compute_signature(
254
262
  accounts_digest: str = "",
255
263
  claude_stats_digest: str = "",
256
264
  claude_weekly_retired_digest: str = "",
265
+ claude_content_revision: str = "",
257
266
  ) -> SnapshotSignature:
258
267
  """Composite data-version signature across cache.db + stats.db (spec §3).
259
268
 
@@ -281,6 +290,7 @@ def compute_signature(
281
290
  codex_ingest_backlog_sig=_codex_ingest_backlog_sig(cache_conn),
282
291
  claude_stats_digest=str(claude_stats_digest),
283
292
  claude_weekly_retired_digest=str(claude_weekly_retired_digest),
293
+ claude_content_revision=str(claude_content_revision),
284
294
  )
285
295
 
286
296