cctally 1.92.3 → 1.93.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.
@@ -0,0 +1,547 @@
1
+ """Journal-to-cache coverage certificate — the pure kernel (#496 S5b, spec §4).
2
+
3
+ A stats rebuild replays every retained Codex quota observation into `cache.db`
4
+ under both cache writer flocks, whether or not the cache already holds them. On
5
+ the maintainer's store that is roughly 1.81 million observations and a 23.0 s
6
+ warm hold of the lock issue #297 blames for `database is locked`. The
7
+ certificate is what lets an intact cache be recognized instead of replayed.
8
+
9
+ **Its single promise is coverage, and nothing wider:**
10
+
11
+ every cache-relevant journal record in the covered prefix has already been
12
+ applied to `cache.db`.
13
+
14
+ It does **not** promise that `cache.db` contains only rows the journal explains.
15
+ `_append_codex_quota_obs` is deliberately best-effort — it catches every
16
+ exception so a failed journal append cannot break a sync, and it runs before the
17
+ cache write — so a swallowed failure leaves a cache row the journal lacks. That
18
+ is divergence in the `cache ⊃ journal` direction, which a coverage-only promise
19
+ does not cover and does not need to.
20
+
21
+ It does **not** promise that any individual row's values are correct. File-account
22
+ rows are first-wins, incarnation rows are MAX-set, and quota replay is
23
+ `INSERT OR IGNORE`, so a wrong pre-existing row survives a replay from byte zero
24
+ and would then be certified by it. A replay cannot prove correctness, so the
25
+ certificate does not claim it.
26
+
27
+ **The identity root binds the ordered vector of extents, not segment names.** A
28
+ late append into a non-last segment changes no name and moves no ordering, so a
29
+ name-only root stays valid while the cache lacks that observation, and the fast
30
+ path would skip a quota replay today's rebuild performs. #511's target
31
+ revalidation is what makes the extent vector stable for the duration of a pass;
32
+ this module is what makes a change to it invalidate the certificate.
33
+
34
+ **A covered extent is always a verified newline boundary, never a raw size.** The
35
+ promise concerns decoded records, so covering a raw torn-tail extent would let
36
+ `_repair_torn_tail` truncate that suffix and append a complete record ending at
37
+ the same size — leaving `(segment, size)` identical while the covered
38
+ contribution changed. The pinned vector therefore carries BOTH the raw `st_size`
39
+ and the complete-line offset per segment, and `coveredHighWater` is bounded to
40
+ the latter.
41
+
42
+ This module imports nothing outside the stdlib, so it is unit-testable without a
43
+ cache or a journal on disk — the same rule `bin/_lib_journal_router.py` follows.
44
+ """
45
+ from __future__ import annotations
46
+
47
+ import hashlib
48
+ import json
49
+
50
+
51
+ #: The `cache_meta` key. Distinct from `codex_quota_projection_certificate`:
52
+ #: coverage binds journal-to-cache, the projection certificate binds
53
+ #: cache-to-stats, and neither may satisfy the other's gate.
54
+ CERTIFICATE_KEY = "codex_journal_coverage_certificate"
55
+
56
+ #: The `cache_meta` key for a recovery pass's in-flight progress, which is
57
+ #: deliberately SEPARATE from the certificate (spec §4.5). A certificate asserts
58
+ #: coverage; progress asserts only how far one pass has got, and a pass that
59
+ #: stops leaves progress behind without ever asserting coverage. Storing the two
60
+ #: under one key would make a partial pass look like a coverage claim.
61
+ PROGRESS_KEY = "codex_journal_coverage_progress"
62
+
63
+ #: Bumped whenever the certificate's own shape or validation semantics change.
64
+ #: A mismatch is one of the states that falls back to a full replay silently.
65
+ #:
66
+ #: Adding the required `appliedThrough` field did NOT bump it, and that is
67
+ #: deliberate rather than an oversight: no released binary ever wrote the
68
+ #: one-coordinate shape, and a certificate lacking the field reads
69
+ #: `REASON_MALFORMED` and falls back to a full replay — the same outcome a bump
70
+ #: would produce. A bump would additionally invalidate certificates written by
71
+ #: an in-development binary that already carries the field, for no gain.
72
+ COVERAGE_VERSION = 1
73
+
74
+ #: Bumped whenever the journal-record-to-cache-row materialization changes —
75
+ #: `_apply_quota_records`, `_apply_file_account_records`, or the §3.5 precedence
76
+ #: rule between them. A certificate written under different semantics describes a
77
+ #: cache this binary would not have produced, so it is rejected rather than
78
+ #: compared.
79
+ INTERPRETATION_VERSION = 1
80
+
81
+ #: The reason strings `certificate_is_valid` returns. They are stable, because
82
+ #: the rebuild record reports them (spec §6.3, "recorded, not silent").
83
+ REASON_OK = "ok"
84
+ REASON_ABSENT = "absent"
85
+ REASON_MALFORMED = "malformed"
86
+ REASON_COVERAGE_VERSION = "coverageVersion"
87
+ REASON_INTERPRETATION_VERSION = "interpretationVersion"
88
+ REASON_PHYSICAL_SEQ = "physicalMutationSeq"
89
+ REASON_IDENTITY_ROOT = "identityRoot"
90
+ REASON_COVERED_HIGH_WATER = "coveredHighWater"
91
+ #: No boundary could be resolved at all — the pass has no high water, or its
92
+ #: high-water segment is absent from the pinned vector. Distinct from
93
+ #: `REASON_IDENTITY_ROOT`, which says the journal moved: this one says the
94
+ #: question was never askable, and reporting the former would tell an operator
95
+ #: the root changed when nothing about the certificate was even consulted.
96
+ REASON_NO_BOUNDARY = "noBoundary"
97
+
98
+
99
+ def _canonical(value) -> str:
100
+ return json.dumps(value, separators=(",", ":"), sort_keys=True,
101
+ ensure_ascii=False)
102
+
103
+
104
+ def normalize_vector(pinned_vector):
105
+ """``pinned_vector`` as a tuple of ``(name, raw_extent, covered_offset)``.
106
+
107
+ Accepts any iterable of three-element sequences so a caller may build it
108
+ from tuples or from lists decoded out of JSON, and rejects anything else
109
+ rather than hashing a shape nobody checked.
110
+ """
111
+ normalized = []
112
+ for item in pinned_vector:
113
+ name, raw_extent, covered_offset = item
114
+ normalized.append((str(name), int(raw_extent), int(covered_offset)))
115
+ return tuple(normalized)
116
+
117
+
118
+ def identity_root(pinned_vector) -> str:
119
+ """SHA-256 over the ORDERED ``(name, raw_extent, covered_offset)`` triples.
120
+
121
+ Ordered, because `list_segments` sorts bootstraps before observations and an
122
+ inserted bootstrap changes the canonical order without changing any existing
123
+ segment. Hashing the order is what makes that insertion invalidate the root.
124
+ """
125
+ payload = _canonical([list(item) for item in normalize_vector(pinned_vector)])
126
+ return hashlib.sha256(payload.encode("utf-8")).hexdigest()
127
+
128
+
129
+ class CoverageOutOfVector(ValueError):
130
+ """A covered boundary the pinned vector does not offer.
131
+
132
+ Raised rather than asserted: `python -O` strips `assert`, and a guard that
133
+ disappears under an optimized interpreter would store an out-of-vector
134
+ certificate instead of refusing to build one. `certificate_is_valid` would
135
+ reject it on first use, so the outcome is a replay either way — but the
136
+ guard is stated as a real check so it holds in every interpreter.
137
+ """
138
+
139
+
140
+ def advance(prior, *, covered, applied_through, pinned_vector,
141
+ physical_seq) -> dict:
142
+ """The certificate describing ``covered`` over ``pinned_vector``.
143
+
144
+ ``covered`` is ``(segment, complete_line_offset)`` — a verified newline
145
+ boundary, never a raw size, for the reason the module docstring gives.
146
+
147
+ ``applied_through`` is the RAW journal coordinate the writer consumed to,
148
+ and it is a separate field because the two are not the same number. The
149
+ ingest cycle advances its scalar cursor to the raw high water, while the
150
+ covered boundary is clamped down to the last line the pass actually decoded.
151
+ Storing only the clamped value made the next writer compare its cursor
152
+ against a boundary that was deliberately smaller, read the difference as a
153
+ gap, and discard the predecessor — so one torn or malformed trailing line
154
+ froze the certificate until the next rebuild. The contiguity check compares
155
+ `appliedThrough`; the coverage claim is `coveredHighWater`; neither operand
156
+ stands in for the other.
157
+
158
+ ``prior`` is accepted and deliberately not merged into the result. A
159
+ certificate is a statement about the CURRENT physical state, not an
160
+ accumulation over previous ones, so a stale field cannot survive an advance.
161
+ It is a parameter only so a caller cannot advance without having read the
162
+ predecessor it is required to validate first.
163
+ """
164
+ del prior
165
+ segment, offset = covered
166
+ applied_segment, applied_offset = applied_through
167
+ vector = normalize_vector(pinned_vector)
168
+ # A mint that `certificate_is_valid` would immediately reject is a caller
169
+ # bug, not a degraded state to fall back from: it means the covered boundary
170
+ # names a segment or an offset the pinned vector does not offer. Refuse here
171
+ # rather than storing it and discovering it one rebuild later.
172
+ if not _covered_within((segment, offset), vector):
173
+ raise CoverageOutOfVector(
174
+ f"coverage {(str(segment), int(offset))!r} is outside the pinned "
175
+ "vector"
176
+ )
177
+ return {
178
+ "coverageVersion": COVERAGE_VERSION,
179
+ "interpretationVersion": INTERPRETATION_VERSION,
180
+ "physicalMutationSeq": int(physical_seq),
181
+ "coveredHighWater": [str(segment), int(offset)],
182
+ "appliedThrough": [str(applied_segment), int(applied_offset)],
183
+ "identityRoot": identity_root(vector),
184
+ }
185
+
186
+
187
+ def prior_is_extendable(prior, *, applied_through) -> "tuple[bool, str]":
188
+ """Whether a contiguous writer may EXTEND ``prior`` rather than discard it.
189
+
190
+ This is deliberately NOT `certificate_is_valid`. An advance's predecessor
191
+ necessarily describes an older, smaller journal — the writer is about to
192
+ certify records that grew a segment after the predecessor was stored — so
193
+ its identity root cannot match this pass's pinned vector and its
194
+ `physicalMutationSeq` predates the bump this transaction is about to make.
195
+ Requiring either would refuse every advance that has ever been correct.
196
+
197
+ What an advance CAN check is the part that does not move with the journal:
198
+ the two version fields, and contiguity against the coordinate the
199
+ predecessor was applied through. The versions matter because
200
+ `_lib_cache_coverage.advance` re-stamps the CURRENT module constants and
201
+ discards `prior`, so extending a certificate written under an older
202
+ `interpretationVersion` would launder it into a current-version one and the
203
+ next rebuild would skip exactly the replay the version bump exists to force.
204
+ """
205
+ if prior is None:
206
+ return False, REASON_ABSENT
207
+ if not isinstance(prior, dict):
208
+ return False, REASON_MALFORMED
209
+ try:
210
+ coverage_version = int(prior["coverageVersion"])
211
+ interpretation_version = int(prior["interpretationVersion"])
212
+ stored_applied = prior["appliedThrough"]
213
+ stored_segment, stored_offset = str(stored_applied[0]), int(
214
+ stored_applied[1])
215
+ except (KeyError, IndexError, TypeError, ValueError):
216
+ return False, REASON_MALFORMED
217
+ if coverage_version != COVERAGE_VERSION:
218
+ return False, REASON_COVERAGE_VERSION
219
+ if interpretation_version != INTERPRETATION_VERSION:
220
+ return False, REASON_INTERPRETATION_VERSION
221
+ try:
222
+ expected = (str(applied_through[0]), int(applied_through[1]))
223
+ except (IndexError, TypeError, ValueError):
224
+ return False, REASON_MALFORMED
225
+ if (stored_segment, stored_offset) != expected:
226
+ return False, REASON_COVERED_HIGH_WATER
227
+ return True, REASON_OK
228
+
229
+
230
+ def _vector_position(coordinate, vector):
231
+ """``(segment_index, offset)`` for ``coordinate``, or None.
232
+
233
+ The ordering comes from the pinned vector rather than from the segment name,
234
+ because `list_segments` sorts bootstraps before observations and a name
235
+ comparison would order those two families lexically instead of canonically.
236
+ """
237
+ try:
238
+ name, offset = str(coordinate[0]), int(coordinate[1])
239
+ except (IndexError, KeyError, TypeError, ValueError):
240
+ return None
241
+ for index, (candidate, _raw_extent, _covered_offset) in enumerate(vector):
242
+ if candidate == name:
243
+ return (index, offset)
244
+ return None
245
+
246
+
247
+ def applied_through_regresses(stored, applied_through, pinned_vector) -> bool:
248
+ """Whether storing ``applied_through`` would move ``stored`` BACKWARD.
249
+
250
+ The recovery mint is the one writer allowed to establish a certificate where
251
+ none existed, and it stores with `prior=None`, so without this check it
252
+ overwrites whatever is present. That is unreachable today only because the
253
+ other certificate writer runs under the ingest lock `cmd_db_rebuild` holds
254
+ exclusively — safety supplied by a lock in another module rather than by the
255
+ mint. This applies the same monotonicity `progress_supersedes` already
256
+ applies to progress records.
257
+
258
+ An incomparable pair answers False: a stored coordinate naming a segment the
259
+ pinned vector does not offer describes a journal this pass is not looking
260
+ at, and `certificate_is_valid` already rejects it on the identity root.
261
+ """
262
+ if not isinstance(stored, dict):
263
+ return False
264
+ try:
265
+ vector = normalize_vector(pinned_vector)
266
+ except (TypeError, ValueError):
267
+ return False
268
+ stored_position = _vector_position(stored.get("appliedThrough"), vector)
269
+ candidate_position = _vector_position(applied_through, vector)
270
+ if stored_position is None or candidate_position is None:
271
+ return False
272
+ return stored_position > candidate_position
273
+
274
+
275
+ def _covered_within(covered, vector) -> bool:
276
+ """Whether ``covered`` names a boundary the pinned vector actually offers."""
277
+ if not covered or len(covered) != 2:
278
+ return False
279
+ name, offset = str(covered[0]), int(covered[1])
280
+ for candidate, _raw_extent, covered_offset in vector:
281
+ if candidate == name:
282
+ return 0 <= offset <= covered_offset
283
+ return False
284
+
285
+
286
+ def certificate_is_valid(cert, *, pinned_vector, physical_seq) -> "tuple[bool, str]":
287
+ """``(verdict, reason)`` for one stored certificate against this pass.
288
+
289
+ The reason is returned rather than logged: every degraded state here falls
290
+ back to a full replay SILENTLY (spec §6.3), and the rebuild record is the one
291
+ surface that reports which state it was.
292
+
293
+ **Validity is an identity check, and the caller must still bound its elision
294
+ by `coveredHighWater`.** A certificate covering only the first of three
295
+ segments is VALID — `_covered_within` asks whether the boundary is one the
296
+ pinned vector offers, not whether it reaches the end of the journal. A caller
297
+ that reads this verdict as "the whole pinned prefix is covered" would skip a
298
+ replay for the two segments the certificate says nothing about.
299
+ """
300
+ if cert is None:
301
+ return False, REASON_ABSENT
302
+ if not isinstance(cert, dict):
303
+ return False, REASON_MALFORMED
304
+ try:
305
+ vector = normalize_vector(pinned_vector)
306
+ except (TypeError, ValueError):
307
+ return False, REASON_MALFORMED
308
+ try:
309
+ coverage_version = int(cert["coverageVersion"])
310
+ interpretation_version = int(cert["interpretationVersion"])
311
+ stored_seq = int(cert["physicalMutationSeq"])
312
+ stored_root = str(cert["identityRoot"])
313
+ covered = cert["coveredHighWater"]
314
+ applied = cert["appliedThrough"]
315
+ str(applied[0]), int(applied[1])
316
+ except (KeyError, IndexError, TypeError, ValueError):
317
+ return False, REASON_MALFORMED
318
+ if coverage_version != COVERAGE_VERSION:
319
+ return False, REASON_COVERAGE_VERSION
320
+ if interpretation_version != INTERPRETATION_VERSION:
321
+ return False, REASON_INTERPRETATION_VERSION
322
+ if stored_seq != int(physical_seq):
323
+ return False, REASON_PHYSICAL_SEQ
324
+ if stored_root != identity_root(vector):
325
+ return False, REASON_IDENTITY_ROOT
326
+ try:
327
+ if not _covered_within(covered, vector):
328
+ return False, REASON_COVERED_HIGH_WATER
329
+ except (TypeError, ValueError):
330
+ return False, REASON_MALFORMED
331
+ return True, REASON_OK
332
+
333
+
334
+ # --------------------------------------------------------------------------
335
+ # recovery progress (spec §4.5)
336
+ # --------------------------------------------------------------------------
337
+
338
+ #: What a resumed recovery pass compares before continuing. Every one of these
339
+ #: is read again after each lock reacquisition, because releasing the flocks
340
+ #: admits a destructive writer.
341
+ PROGRESS_FIELDS = (
342
+ "passId", "startedAt", "chunks", "identityRoot",
343
+ "physicalMutationSeq", "sourceRootsDigest", "coveredHighWater",
344
+ )
345
+
346
+ #: `resume` — the stored progress is this pass's own and still describes the
347
+ #: state it left behind, so the pass may keep going.
348
+ #:
349
+ #: **The record is a revalidation token, not a resume point, and the wording
350
+ #: matters because an earlier draft claimed the latter.** A fresh process always
351
+ #: starts at chunk zero: `_run_bounded_recovery` initializes `chunk_index = 0`
352
+ #: unconditionally and revalidates only from chunk 1 onward, so the stored
353
+ #: `chunks` is read by `progress_supersedes` for compare-and-swap ordering and
354
+ #: by nothing else. Recovery is therefore resumable WITHIN one process and
355
+ #: restart-from-zero ACROSS processes.
356
+ #:
357
+ #: That is the conservative direction and it was chosen over implementing the
358
+ #: cross-process resume. The reason is NOT that revalidation would have to move
359
+ #: ahead of chunk zero — an earlier draft said that and it does not follow, since
360
+ #: a process resuming at chunk k has `chunk_index = k > 0` and the existing
361
+ #: `if chunk_index > 0` revalidation already fires in its first transaction,
362
+ #: with §3.5's precedence satisfied for the earlier chunks by the earlier
363
+ #: process and every apply idempotent. The real awkwardness is that `chunks`
364
+ #: indexes a plan derived from THIS process's `quota_raw`, and a journal that
365
+ #: grew between the two passes gives the second process a different plan for
366
+ #: the same index — so the stored number would have to be re-expressed as a
367
+ #: journal coordinate to mean anything across processes. The identity-root
368
+ #: comparison already catches that mismatch, so nothing is unsafe today; the
369
+ #: cross-process resume is simply not worth the extra invariant, because
370
+ #: restarting is idempotent and costs repeated work and nothing else.
371
+ RESUME = "resume"
372
+ #: `restart` — no usable progress. Either a destructive writer deleted it in the
373
+ #: same transaction as its deletes, or it describes a state this pass cannot
374
+ #: continue from. The pass starts again from chunk zero, which is always sound
375
+ #: because every apply is idempotent on its natural key.
376
+ RESTART = "restart"
377
+ #: `yield` — a NEWER pass owns the progress record. This pass stops rather than
378
+ #: restarting, because two passes that each restart on seeing the other make no
379
+ #: progress at all.
380
+ YIELD = "yield"
381
+
382
+
383
+ def make_progress(*, pass_id, started_at, chunks, identity_root,
384
+ physical_seq, source_roots_digest, covered) -> dict:
385
+ """One pass's progress record, in the canonical field order.
386
+
387
+ There is no applied-record count. An earlier shape carried one and nothing
388
+ ever read it, and a durable field nobody consumes is a field a later reader
389
+ will mistake for state the mechanism depends on.
390
+ """
391
+ segment, offset = covered
392
+ return {
393
+ "passId": str(pass_id),
394
+ "startedAt": int(started_at),
395
+ "chunks": int(chunks),
396
+ "identityRoot": str(identity_root),
397
+ "physicalMutationSeq": int(physical_seq),
398
+ "sourceRootsDigest": str(source_roots_digest),
399
+ "coveredHighWater": [str(segment), int(offset)],
400
+ }
401
+
402
+
403
+ def source_roots_digest(root_keys) -> str:
404
+ """A stable digest over the cache's Codex source roots.
405
+
406
+ `_clear_codex_derived_rows` empties `codex_source_roots` along with the
407
+ quota rows, so this is a second, independent witness that the destructive
408
+ path ran — one that does not depend on the progress delete having happened.
409
+ """
410
+ payload = _canonical(sorted(str(key) for key in root_keys))
411
+ return hashlib.sha256(payload.encode("utf-8")).hexdigest()
412
+
413
+
414
+ #: How far AHEAD of this pass's own start a foreign progress record may sit and
415
+ #: still be treated as a live concurrent pass rather than an orphan.
416
+ #:
417
+ #: `startedAt` is a wall clock compared with `>` across processes, and there is
418
+ #: no pid, heartbeat or TTL behind it. Two passes on one machine read the same
419
+ #: clock, so a genuinely live competitor's start time is at most seconds ahead.
420
+ #: A record arbitrarily far in the future means the clock stepped BACKWARD
421
+ #: between the two passes — an NTP correction or a VM restore — and without this
422
+ #: bound every later pass would look older than that orphan and yield to it on
423
+ #: every rebuild until the clock caught up. One hour is far past any real
424
+ #: inter-pass skew and far short of the clock steps that produce the failure.
425
+ PROGRESS_YIELD_MAX_SKEW_US = 3_600 * 1_000_000
426
+
427
+ #: A foreign record so far in the future that it cannot describe a live pass.
428
+ REASON_ORPHANED_PASS = "orphanedPass"
429
+
430
+
431
+ def resume_verdict(stored, *, pass_id, started_at, identity_root,
432
+ physical_seq, source_roots_digest) -> "tuple[str, str, bool]":
433
+ """``(RESUME | RESTART | YIELD, reason, concurrent_writer)`` after a lock
434
+ reacquisition.
435
+
436
+ Ordering matters. A FOREIGN newer pass is checked before anything else,
437
+ because yielding to it must not be turned into a restart by some other
438
+ mismatch; a foreign OLDER pass is simply overwritten, which is what makes
439
+ the compare-and-swap monotonic.
440
+
441
+ **DELIBERATE DEVIATION from spec §4.5, which restarts on any mismatch of
442
+ the four compared quantities. It narrows ONE of the four, not two.** A moved
443
+ `physicalMutationSeq` is REPORTED — the third element of the result — but
444
+ does not by itself restart the pass; a changed `sourceRootsDigest` restarts
445
+ like every other mismatch. The liveness argument covers only the sequence.
446
+ An ordinary rollout batch bumps the sequence on every status-line tick, and
447
+ what it reports is an ADDITIVE writer whose rows this pass's `INSERT OR
448
+ IGNORE` applies over without any loss of coverage, and whose bump the final
449
+ transaction's mint reads anyway, so it cannot produce a stale-valid
450
+ certificate. Restarting on it would abandon a pass every time a tick landed
451
+ mid-recovery, and after the restart limit would report an uncovered
452
+ remainder for a cache that has no shortfall at all.
453
+
454
+ **The digest does not behave that way and must not share the narrowing.**
455
+ It is computed over the SET of `source_root_key` values, so an ordinary
456
+ batch's `INSERT … ON CONFLICT DO UPDATE SET last_seen_utc` does not move it;
457
+ only a new root appearing or `_prune_inactive_codex_source_roots` deleting
458
+ one does. `_clear_codex_derived_rows` empties `codex_source_roots` along
459
+ with the quota rows, which makes the digest a second, independent witness
460
+ that the destructive path ran — one that does not depend on the progress
461
+ delete having happened. Folding that witness into a report would leave the
462
+ single mechanism designed to catch a destructive clear whose progress delete
463
+ was missed present and deliberately not acted on, so it restarts, and
464
+ restarting on it costs essentially nothing.
465
+
466
+ Everything that does restart is a restart rather than a failure. Restarting
467
+ is always sound — every apply is idempotent on its natural key — so the cost
468
+ of being wrong in that direction is repeated work, while the cost of
469
+ resuming over a cleared cache is a certificate claiming coverage the cache
470
+ does not have.
471
+ """
472
+ if stored is None:
473
+ return RESTART, REASON_ABSENT, False
474
+ if not isinstance(stored, dict):
475
+ return RESTART, REASON_MALFORMED, False
476
+ try:
477
+ stored_pass = str(stored["passId"])
478
+ stored_started = int(stored["startedAt"])
479
+ int(stored["chunks"])
480
+ stored_root = str(stored["identityRoot"])
481
+ stored_seq = int(stored["physicalMutationSeq"])
482
+ stored_digest = str(stored["sourceRootsDigest"])
483
+ except (KeyError, TypeError, ValueError):
484
+ return RESTART, REASON_MALFORMED, False
485
+ if stored_pass != str(pass_id):
486
+ if stored_started > int(started_at):
487
+ if stored_started - int(started_at) > PROGRESS_YIELD_MAX_SKEW_US:
488
+ return RESTART, REASON_ORPHANED_PASS, False
489
+ return YIELD, "newerPass", True
490
+ # A dead pass's leftover record is not a concurrent writer, and
491
+ # reporting one would put a writer that does not exist on the rebuild
492
+ # record.
493
+ return RESTART, "foreignPass", False
494
+ concurrent = stored_seq != int(physical_seq)
495
+ if stored_digest != str(source_roots_digest):
496
+ return RESTART, "sourceRootsDigest", concurrent
497
+ if stored_root != str(identity_root):
498
+ return RESTART, REASON_IDENTITY_ROOT, concurrent
499
+ return RESUME, REASON_OK, concurrent
500
+
501
+
502
+ def progress_supersedes(stored, candidate) -> bool:
503
+ """Whether ``candidate`` may replace ``stored`` under the monotonic CAS.
504
+
505
+ An older worker cannot overwrite a newer pass's progress, and a pass cannot
506
+ move its own progress backwards.
507
+ """
508
+ if stored is None:
509
+ return True
510
+ try:
511
+ stored_pass = str(stored["passId"])
512
+ stored_started = int(stored["startedAt"])
513
+ stored_chunks = int(stored["chunks"])
514
+ candidate_pass = str(candidate["passId"])
515
+ candidate_started = int(candidate["startedAt"])
516
+ candidate_chunks = int(candidate["chunks"])
517
+ except (KeyError, TypeError, ValueError):
518
+ return True
519
+ if stored_pass == candidate_pass:
520
+ return candidate_chunks > stored_chunks
521
+ return candidate_started > stored_started
522
+
523
+
524
+ def chunk_spans(sizes, *, byte_cap, record_cap):
525
+ """``[(start, stop, encoded_bytes), ...]`` over ``sizes``, capped BOTH ways.
526
+
527
+ Capping by records alone lets one chunk of large observations blow the
528
+ memory bound, and capping by bytes alone lets a chunk of tiny ones carry far
529
+ more rows than one transaction should. A single record larger than the byte
530
+ cap still gets its own chunk rather than none — refusing it would stall the
531
+ pass on a record it is required to apply.
532
+ """
533
+ spans = []
534
+ start = 0
535
+ total = 0
536
+ for index, size in enumerate(sizes):
537
+ size = int(size)
538
+ if index > start and (
539
+ total + size > int(byte_cap) or index - start >= int(record_cap)
540
+ ):
541
+ spans.append((start, index, total))
542
+ start = index
543
+ total = 0
544
+ total += size
545
+ if start < len(sizes):
546
+ spans.append((start, len(sizes), total))
547
+ return spans
@@ -232,6 +232,12 @@ class DoctorState:
232
232
  # DOCTOR_WAL_WARN_BYTES (2x the WAL cap) — only when the journal_size_limit
233
233
  # + forced-checkpoint machinery has genuinely failed to contain the WAL.
234
234
  cache_db_wal_bytes: Optional[int] = None
235
+ # #496 S5b: the durable incomplete-quota-projection flag carried inside the
236
+ # published stats generation. True = every quota-projection read is refused
237
+ # until a reconciliation runs; False = the generation is complete; None =
238
+ # there is no epoch-1009 index to ask (absent file, pre-1009 index, or an
239
+ # unreadable DB), which the check reports as not applicable.
240
+ stats_quota_projection_incomplete: Optional[bool] = None
235
241
  # #315: read-only PRAGMA page_count/freelist_count evidence. The pure
236
242
  # db.reclaimable check warns when free pages reach 25% of cache.db and
237
243
  # points at the already-guarded explicit vacuum command. None means the
@@ -923,8 +929,12 @@ def _check_db_version_ahead(s: DoctorState) -> CheckResult:
923
929
  if epoch is None:
924
930
  # Fallback kept in lockstep with _cctally_core.STATS_INDEX_EPOCH; the
925
931
  # gather layer injects the real constant, so this only guards a hand-
926
- # built DoctorState that omitted it.
927
- epoch = 1000
932
+ # built DoctorState that omitted it. It sat at 1000 against a current
933
+ # constant of 1008 for eight epochs, which made this guard report a
934
+ # current index as a mismatch; corrected with the 1009 bump
935
+ # (#496 S5b §6.1). It stays a literal because this kernel is pure and
936
+ # must not import `_cctally_core`.
937
+ epoch = 1009
928
938
  mismatch = uv > legacy_head and uv != epoch
929
939
  return {"user_version": uv, "legacy_head": legacy_head, "epoch": epoch,
930
940
  "mismatch": mismatch}
@@ -2702,6 +2712,47 @@ def _check_journal_protocol(s: DoctorState) -> CheckResult:
2702
2712
  )
2703
2713
 
2704
2714
 
2715
+ def _check_journal_quota_projection(s: DoctorState) -> CheckResult:
2716
+ """Report a published stats generation whose quota projection is incomplete
2717
+ (#496 S5b §4.7).
2718
+
2719
+ The flag is set by a rebuild whose Codex quota cache recovery stopped short,
2720
+ and only two things clear it: a reconciliation armed by `cctally cache-sync`
2721
+ or the dashboard server, or a later rebuild whose coverage came back
2722
+ complete. No ingest path clears it, so it can stay set indefinitely while
2723
+ every quota-projection read is refused — and every consumer surface renders
2724
+ that refusal as absent or stale data. Nothing else reported it, which is why
2725
+ this leg exists.
2726
+
2727
+ WARN rather than FAIL: the index is valid, the remedy is one ordinary
2728
+ command, and no data is lost. This check is read-only and never reconciles.
2729
+ """
2730
+ incomplete = s.stats_quota_projection_incomplete
2731
+ details = {"incomplete": incomplete}
2732
+ if incomplete is True:
2733
+ return CheckResult(
2734
+ id="journal.quota_projection", title="Quota projection",
2735
+ severity="warn",
2736
+ summary="incomplete — quota projection reads are refused",
2737
+ remediation=(
2738
+ "Run `cctally cache-sync` to reconcile the quota projection "
2739
+ "against the journal"
2740
+ ),
2741
+ details=details,
2742
+ )
2743
+ if incomplete is False:
2744
+ return CheckResult(
2745
+ id="journal.quota_projection", title="Quota projection",
2746
+ severity="ok", summary="complete", remediation=None,
2747
+ details=details,
2748
+ )
2749
+ return CheckResult(
2750
+ id="journal.quota_projection", title="Quota projection",
2751
+ severity="ok", summary="not applicable", remediation=None,
2752
+ details=details,
2753
+ )
2754
+
2755
+
2705
2756
  # Each entry is (category_id, category_title, ((check_id, evaluator_fn_name), ...)).
2706
2757
  # The dotted check_id is the stable JSON-contract ID (spec §5.2) AND the
2707
2758
  # fingerprint identity-slice key (spec §5.5). When an evaluator raises,
@@ -2987,6 +3038,7 @@ _CATEGORY_DEFINITIONS: tuple[tuple[str, str, tuple[tuple[str, str], ...]], ...]
2987
3038
  ("journal.writer_guard", "_check_journal_writer_guard"),
2988
3039
  ("journal.conflicts", "_check_journal_conflicts"),
2989
3040
  ("journal.protocol", "_check_journal_protocol"),
3041
+ ("journal.quota_projection", "_check_journal_quota_projection"),
2990
3042
  )),
2991
3043
  ("data", "Data", (
2992
3044
  ("data.latest_snapshot_age", "_check_data_latest_snapshot_age"),