cctally 1.111.0 → 1.113.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 (37) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +6 -6
  3. package/bin/_cctally_cache.py +14 -7
  4. package/bin/_cctally_core.py +9 -1
  5. package/bin/_cctally_dashboard_envelope.py +7 -6
  6. package/bin/_cctally_dashboard_sources.py +90 -55
  7. package/bin/_cctally_doctor.py +73 -27
  8. package/bin/_cctally_forecast.py +2 -1
  9. package/bin/_cctally_journal.py +147 -6
  10. package/bin/_cctally_milestones.py +29 -3
  11. package/bin/_cctally_quota.py +10 -1
  12. package/bin/_cctally_record.py +136 -4
  13. package/bin/_cctally_rederive.py +311 -8
  14. package/bin/_cctally_source_analytics.py +20 -8
  15. package/bin/_cctally_tui.py +546 -45
  16. package/bin/_lib_codex_conversation_query.py +40 -4
  17. package/bin/_lib_codex_metadata.py +57 -11
  18. package/bin/_lib_cost_provenance.py +303 -0
  19. package/bin/_lib_dashboard_sources.py +263 -28
  20. package/bin/_lib_doctor.py +2 -2
  21. package/bin/_lib_ingest_frontier.py +137 -38
  22. package/bin/_lib_pricing.py +29 -1
  23. package/bin/_lib_rederive.py +704 -36
  24. package/bin/_lib_snapshot_cache.py +332 -9
  25. package/dashboard/static/assets/{ConversationsView-BB9vUiL6.js → ConversationsView-CEZ-BUyi.js} +1 -1
  26. package/dashboard/static/assets/{DoctorModal-DFY2iPDf.js → DoctorModal-CE1oH_Zy.js} +1 -1
  27. package/dashboard/static/assets/ModalRoot-D5Y8IFUG.js +1 -0
  28. package/dashboard/static/assets/{ProjectsDrillPanel-DAv39uSo.js → ProjectsDrillPanel-B3DZoCeD.js} +1 -1
  29. package/dashboard/static/assets/{SourceDetailModal-DSAWo2Fp.js → SourceDetailModal-CHp9EgLP.js} +1 -1
  30. package/dashboard/static/assets/{UpdateModal-R_9dOVnh.js → UpdateModal-5iKST6jo.js} +1 -1
  31. package/dashboard/static/assets/{index-DdU62sov.js → index-Cm0VTxJC.js} +2 -2
  32. package/dashboard/static/assets/index-Dt4tqf43.css +1 -0
  33. package/dashboard/static/assets/{outlineNavigation-BOCLcRa6.js → outlineNavigation-DkXGL8J6.js} +1 -1
  34. package/dashboard/static/dashboard.html +2 -2
  35. package/package.json +2 -1
  36. package/dashboard/static/assets/ModalRoot-DvRKHbQE.js +0 -1
  37. package/dashboard/static/assets/index-COhxe3vU.css +0 -1
@@ -44,8 +44,10 @@ reason.
44
44
  """
45
45
  from __future__ import annotations
46
46
 
47
+ import contextlib
47
48
  import datetime as dt
48
49
  import hashlib
50
+ import itertools
49
51
  import os
50
52
  import sqlite3
51
53
  import sys
@@ -274,6 +276,210 @@ def compute_signature(
274
276
  )
275
277
 
276
278
 
279
+ # === #857 — the Codex quota-dependency identity =============================
280
+
281
+
282
+ #: Bumped whenever the leg set below changes, so an identity captured by an
283
+ #: older build can never compare equal to one read by a newer rule.
284
+ CODEX_QUOTA_DEPENDENCY_VERSION = "codex-quota-dependency-v1"
285
+
286
+ #: `_cctally_quota._DASHBOARD_PROJECTION_CERTIFICATE_KEY`, spelled here because
287
+ #: this kernel may not import the quota glue; a test pins the two together.
288
+ _CODEX_PROJECTION_CERTIFICATE_KEY = "codex_quota_projection_certificate"
289
+
290
+ @contextlib.contextmanager
291
+ def one_read_snapshot(conn: sqlite3.Connection):
292
+ """Run the enclosed reads on ONE database snapshot.
293
+
294
+ Joins a transaction the caller already holds — the dashboard's pinned
295
+ source build, or the dispatch read that wraps its signature and its Codex
296
+ quota-dependency identity in one — rather than nesting one; otherwise opens
297
+ a deferred read transaction and rolls it back, leaving the connection as it
298
+ was found. Without it each statement reads its own snapshot, so a commit
299
+ landing between two legs yields an identity no database state ever had. A
300
+ deferred ``BEGIN`` takes no lock and pins no snapshot until the first read,
301
+ so work done inside the block before that read holds nothing. A failing
302
+ ``BEGIN`` raises ``sqlite3.Error`` to the caller, whose own handler decides.
303
+ """
304
+ if conn.in_transaction:
305
+ yield
306
+ return
307
+ conn.execute("BEGIN")
308
+ try:
309
+ yield
310
+ finally:
311
+ try:
312
+ conn.rollback()
313
+ except sqlite3.Error:
314
+ pass
315
+
316
+
317
+ def codex_cache_file_identity(path: object) -> tuple[int, int] | None:
318
+ """``(st_dev, st_ino)`` of the file ``path`` names NOW (#857).
319
+
320
+ ``None`` when it cannot be stat'ed. A path is only a name: this describes
321
+ whichever file holds it at the instant of the call, which is why
322
+ `codex_quota_dependency_identity` brackets its reads with it rather than
323
+ trusting one stat.
324
+ """
325
+ try:
326
+ file_stat = os.stat(path) # type: ignore[arg-type]
327
+ except (OSError, TypeError, ValueError):
328
+ return None
329
+ return (int(file_stat.st_dev), int(file_stat.st_ino))
330
+
331
+
332
+ def codex_quota_observation_legs(
333
+ conn: sqlite3.Connection,
334
+ ) -> tuple[int, str] | None:
335
+ """The cache.db half of the quota-observation identity (#583 S5, #857).
336
+
337
+ ``(quota_window_change_log high-water, codex_window_attribution_revision)``,
338
+ read on one snapshot. Its callers are the doctor's quota-probe signal, the
339
+ dashboard's cross-build quota memo (`_codex_quota_reuse_identity`) and the
340
+ quota-dependency identity below; each leg's query is stated once, the
341
+ revision's in `_read_codex_window_attribution_revision`, which the
342
+ accounting population signature reads too.
343
+
344
+ The high-water mark is read from ``sqlite_sequence``, not ``MAX(seq)``: the
345
+ projector PRUNES consumed ledger entries, and ``AUTOINCREMENT`` keeps its
346
+ high-water across that ``DELETE``, so a prune never reads as a regression.
347
+ Every insert, delete and semantic update of ``quota_window_snapshots`` is
348
+ recorded by the ledger triggers, including a write that forgot to advance
349
+ ``codex_physical_mutation_seq``. The attribution revision covers the overlay
350
+ the journal applies with no ``quota_window_snapshots`` write at all.
351
+
352
+ ``None`` means "cannot establish identity" and every caller must treat it
353
+ as a cold read: an absent or unreadable ledger is not an idle ledger.
354
+ """
355
+ try:
356
+ with one_read_snapshot(conn):
357
+ table = conn.execute(
358
+ "SELECT 1 FROM sqlite_master WHERE type='table' "
359
+ "AND name='quota_window_change_log'"
360
+ ).fetchone()
361
+ if table is None:
362
+ return None
363
+ seq_row = conn.execute(
364
+ "SELECT seq FROM sqlite_sequence "
365
+ "WHERE name='quota_window_change_log'"
366
+ ).fetchone()
367
+ revision = _read_codex_window_attribution_revision(conn)
368
+ except sqlite3.Error:
369
+ return None
370
+ try:
371
+ high_water = 0 if seq_row is None else int(seq_row[0])
372
+ except (TypeError, ValueError):
373
+ return None
374
+ return (high_water, "" if revision is None else revision)
375
+
376
+
377
+ def _codex_projection_certificate_digest(conn: sqlite3.Connection) -> str:
378
+ """SHA-256 of the certificate's stored bytes, ``""`` when absent.
379
+
380
+ Raises ``sqlite3.Error``; the identity's handler decides what that means.
381
+ """
382
+ row = conn.execute(
383
+ "SELECT value FROM cache_meta WHERE key=?",
384
+ (_CODEX_PROJECTION_CERTIFICATE_KEY,),
385
+ ).fetchone()
386
+ if row is None or row[0] is None:
387
+ return ""
388
+ return hashlib.sha256(str(row[0]).encode("utf-8")).hexdigest()
389
+
390
+
391
+ def codex_quota_dependency_identity(
392
+ conn: sqlite3.Connection,
393
+ *,
394
+ opened_file: "tuple[int, int] | None",
395
+ ) -> tuple | None:
396
+ """What a Codex quota verdict depends on beyond the signature legs (#857).
397
+
398
+ The dispatch signature carries ``codex_physical_mutation_seq`` and the
399
+ accounting ledger, but not three inputs that can change a Codex cycle or
400
+ projection verdict on their own: the dedicated quota change ledger (a
401
+ writer that forgot the physical counter, or manual repair), the window
402
+ attribution revision (which also invalidates the projection certificate
403
+ with no row write), and the projection certificate itself (a
404
+ certificate-only recovery or loss). Nor does it see the database file being
405
+ replaced by a restored copy whose counters match.
406
+
407
+ Legs: the version tag, ``st_dev`` / ``st_ino`` of the file this connection
408
+ reads, the two observation legs above, and a digest of the certificate's
409
+ stored bytes (empty when absent). The digest covers only the certificate's
410
+ CONTENTS. Whether those contents are valid is decided with inputs this
411
+ identity does not carry: ``codex_physical_mutation_seq``, which the
412
+ certificate must match, is a dispatch-signature leg and part of the Codex
413
+ ``data_version``; and the stats.db ``quota_projection_state`` it is checked
414
+ against is covered by ``codex_stats_digest``, also in both. The contents
415
+ are the one part neither of those sees, so they are this identity's leg,
416
+ and the key and the version together cover the rest.
417
+
418
+ File identity is ``(st_dev, st_ino)`` rather than the path string: the
419
+ dashboard compares an identity read on its pinned read-write connection with
420
+ one read at dispatch on another connection, and the path SQLite reports is
421
+ a spelling of the same file that two openers need not share.
422
+
423
+ **What is bound.** ``opened_file`` is REQUIRED: the
424
+ ``codex_cache_file_identity`` of the configured database path, observed by
425
+ the caller BEFORE it opened ``conn``. After this function's last read, the
426
+ path ``PRAGMA database_list`` reports for ``conn`` — resolved once, here —
427
+ is stat'ed once more, and the identity exists only when that stat names the
428
+ same file. SQLite opens the main database file when the connection is
429
+ created and reads through that one descriptor for its lifetime; a stat of
430
+ a path, by contrast, describes whatever file holds the name at that
431
+ instant. So agreement between a stat taken before the open and a stat of
432
+ the connection's own path taken after every read proves the connection
433
+ read the file both name. The binding has two blind spots, either mistaken
434
+ for no replacement only when every other leg of the identity matches as
435
+ well: a file moved away and then back to that same inode inside the
436
+ bracket; and, on a filesystem that reuses inode numbers (APFS does not), a
437
+ replacement that received the number of the file it replaced once that
438
+ file was freed, whose identity then names the same ``(st_dev, st_ino)`` as
439
+ a stamp taken on its predecessor. Disagreement is ``None``: the file was
440
+ replaced between the open and the last read (a stat then names the NEW
441
+ inode beside the OLD rows), or the connection was opened on a different
442
+ file than the configured path names.
443
+
444
+ **There is no fallback.** ``opened_file=None`` — the pre-open stat failed,
445
+ whether because the name was absent (a fresh install, or a restore caught
446
+ mid-rename) or for any other reason — yields ``None``: nothing observed
447
+ before the open, so nothing establishes which file the connection opened,
448
+ and a bracket that began after the open could stat a replacement twice
449
+ around reads from the replaced file. A caller that needs an identity where
450
+ the file did not exist yet must observe the file its first open created
451
+ and open AGAIN (the dashboard's dispatch read does exactly that). A
452
+ replacement AFTER the bracket needs nothing more: the connection keeps
453
+ reading the file the identity names, and the next read, on a new
454
+ connection, names the new file.
455
+
456
+ Every leg is read on ONE snapshot (`one_read_snapshot`), so a commit
457
+ landing between two legs cannot produce an identity no state ever had.
458
+ Every read is O(1) — ``PRAGMA database_list`` once, one ``stat``, a catalog
459
+ lookup and three primary-key reads — because the dashboard pays it on
460
+ every idle tick. It is STABLE while nothing changes: none of the legs moves
461
+ on a read, a WAL checkpoint, or a ledger prune. ``None`` means "cannot
462
+ establish identity", which every caller must treat as "rebuild".
463
+ """
464
+ if opened_file is None:
465
+ return None
466
+ path = codex_main_database_path(conn)
467
+ if not path:
468
+ return None
469
+ try:
470
+ with one_read_snapshot(conn):
471
+ legs = codex_quota_observation_legs(conn)
472
+ certificate = _codex_projection_certificate_digest(conn)
473
+ except sqlite3.Error:
474
+ return None
475
+ if legs is None:
476
+ return None
477
+ after = codex_cache_file_identity(path)
478
+ if after is None or after != tuple(opened_file):
479
+ return None
480
+ return (CODEX_QUOTA_DEPENDENCY_VERSION, *after, *legs, certificate)
481
+
482
+
277
483
  # === Task 0.2 — new-entry timestamp watermark ==============================
278
484
 
279
485
 
@@ -1103,26 +1309,99 @@ class CodexAccountingCacheResult:
1103
1309
  #: all. `None` means "cannot establish an identity", which every consumer
1104
1310
  #: must treat as a cold read rather than as a hit.
1105
1311
  population_signature: tuple | None = None
1312
+ #: #857: the capture-owned accounting provenance token — the never-reused
1313
+ #: name of the population in `entries` (`build_cached_codex_accounting`
1314
+ #: states when it is minted and when preserved). The dashboard stamps it on
1315
+ #: the generation this capture builds and admits a retained generation
1316
+ #: only while its stamp equals `codex_accounting_consumed_provenance()`.
1317
+ provenance_token: int | None = None
1106
1318
 
1107
1319
 
1108
1320
  _CODEX_ACCOUNTING_CACHE_STATE: dict[str, object] = _ObservedSnapshotDict()
1109
1321
  _CODEX_ACCOUNTING_MAX_DIRTY_PATHS = 300
1110
1322
 
1323
+ #: #857: the server-only ``clock_data`` key a Codex generation carries its
1324
+ #: capture's accounting provenance token under (`None` for a build that
1325
+ #: bypassed the qualified accounting read, which no admission gate accepts).
1326
+ CODEX_ACCOUNTING_PROVENANCE_KEY = "codex_accounting_provenance"
1327
+ #: The state-dictionary key the token of the population the dictionary holds
1328
+ #: lives under. Inside the dictionary on purpose: a checkpoint copies it and a
1329
+ #: restore puts it back together with exactly the state it names, never apart.
1330
+ _CODEX_ACCOUNTING_PROVENANCE_STATE_KEY = "provenance_token"
1331
+ #: The token allocator. Never rewound: not by a reset, not by a restore and not
1332
+ #: by memory eviction, so no two populations this process ever held — however
1333
+ #: their ``population_signature`` generations compare after a clear — share a
1334
+ #: token.
1335
+ _CODEX_ACCOUNTING_PROVENANCE_TOKENS = itertools.count(1)
1336
+ #: The token of the population the process accounting cache most recently
1337
+ #: CONSUMED — set by every successful `build_cached_codex_accounting` call.
1338
+ #: Deliberately OUTSIDE `_CODEX_ACCOUNTING_CACHE_STATE`: snapshot memory
1339
+ #: enforcement clears that dictionary after a published build, and an eviction
1340
+ #: consumes nothing, so it must not invalidate the generation just published.
1341
+ #: A reset or a checkpoint restore does not move it either. When the failed
1342
+ #: build's capture minted, a generation stamped with the checkpoint's token
1343
+ #: predates accounting this process consumed once and stays refused until a
1344
+ #: capture names the population again; when that capture preserved the token,
1345
+ #: it consumed nothing, and the checkpoint's generation correctly stays current.
1346
+ _CODEX_ACCOUNTING_CONSUMED_PROVENANCE: int | None = None
1347
+
1348
+
1349
+ def codex_accounting_consumed_provenance() -> int | None:
1350
+ """The token of the accounting population this process last consumed.
1351
+
1352
+ #857. An O(1) read of a module global — no database, no state walk — so
1353
+ every Codex admission gate can compare a retained generation's stamp
1354
+ against it on every tick. ``None`` until the first qualified capture.
1355
+ Not owner-asserted because it mutates nothing. It is safe only because
1356
+ every writer is owner-asserted and every gate reads on the owner thread, so
1357
+ no read can land between a capture's state write and its consume call.
1358
+ """
1359
+ return _CODEX_ACCOUNTING_CONSUMED_PROVENANCE
1360
+
1361
+
1362
+ def _mint_codex_accounting_provenance() -> int:
1363
+ """A token no earlier population of this process has carried (#857)."""
1364
+ return next(_CODEX_ACCOUNTING_PROVENANCE_TOKENS)
1365
+
1366
+
1367
+ def _consume_codex_accounting_provenance(token: int) -> int:
1368
+ """Record ``token`` as the last consumed population's name and return it."""
1369
+ global _CODEX_ACCOUNTING_CONSUMED_PROVENANCE
1370
+ _CODEX_ACCOUNTING_CONSUMED_PROVENANCE = token
1371
+ return token
1372
+
1111
1373
 
1112
1374
  def reset_codex_accounting_cache_state() -> None:
1113
- """Drop #582's value-only Codex accounting cache and ledger cursor."""
1375
+ """Drop #582's value-only Codex accounting cache and ledger cursor.
1376
+
1377
+ #857: the consumed provenance token and its allocator are untouched. A
1378
+ reset consumes nothing, and the next capture goes cold and mints a token
1379
+ no earlier population carried.
1380
+ """
1114
1381
  _assert_owner()
1115
1382
  _CODEX_ACCOUNTING_CACHE_STATE.clear()
1116
1383
 
1117
1384
 
1118
1385
  def checkpoint_codex_accounting_cache_state() -> dict[str, object]:
1119
- """Copy the value-only state so a failed source build can roll back."""
1386
+ """Copy the value-only state so a failed source build can roll back.
1387
+
1388
+ The copy carries the population's provenance token (#857), so a restore
1389
+ puts back exactly the state that token names.
1390
+ """
1120
1391
  _assert_owner()
1121
1392
  return dict(_CODEX_ACCOUNTING_CACHE_STATE)
1122
1393
 
1123
1394
 
1124
1395
  def restore_codex_accounting_cache_state(state: dict[str, object]) -> None:
1125
- """Restore a checkpoint after downstream source construction fails."""
1396
+ """Restore a checkpoint after downstream source construction fails.
1397
+
1398
+ #857: the state's own token comes back with it, but the CONSUMED token does
1399
+ not move back and the allocator is never rewound. If the failed build's
1400
+ capture minted, a generation stamped with the checkpoint's token predates
1401
+ accounting that build consumed and stays refused until a capture names the
1402
+ population again; if it preserved the token, it consumed nothing and that
1403
+ generation correctly stays current.
1404
+ """
1126
1405
  _assert_owner()
1127
1406
  _CODEX_ACCOUNTING_CACHE_STATE.clear()
1128
1407
  _CODEX_ACCOUNTING_CACHE_STATE.update(state)
@@ -1175,18 +1454,31 @@ def _codex_window_attribution_revision(conn: sqlite3.Connection) -> str | None:
1175
1454
  keyed on the ledger alone would keep serving a population built before the
1176
1455
  operator's attribution landed. Carrying the revision turns that window into
1177
1456
  an unnecessary miss, which is the direction this signature is written in.
1457
+
1458
+ An unreadable revision reads as ``None`` here, which the signature treats
1459
+ as one more value; the #857 identity reads the same query through
1460
+ `_read_codex_window_attribution_revision` instead, because there an
1461
+ unreadable leg must refuse the identity rather than become a value.
1178
1462
  """
1179
1463
  try:
1180
- row = conn.execute(
1181
- "SELECT value FROM cache_meta "
1182
- "WHERE key='codex_window_attribution_revision'"
1183
- ).fetchone()
1464
+ return _read_codex_window_attribution_revision(conn)
1184
1465
  except sqlite3.Error:
1185
1466
  return None
1467
+
1468
+
1469
+ def _read_codex_window_attribution_revision(
1470
+ conn: sqlite3.Connection,
1471
+ ) -> str | None:
1472
+ """The ONE statement of the attribution-revision read; raises on error."""
1473
+ row = conn.execute(
1474
+ "SELECT value FROM cache_meta "
1475
+ "WHERE key='codex_window_attribution_revision'"
1476
+ ).fetchone()
1186
1477
  return None if row is None or row[0] is None else str(row[0])
1187
1478
 
1188
1479
 
1189
- def _codex_main_database_path(conn: sqlite3.Connection) -> str | None:
1480
+ def codex_main_database_path(conn: sqlite3.Connection) -> str | None:
1481
+ """The path SQLite reports for ``conn``'s ``main`` database, or ``None``."""
1190
1482
  try:
1191
1483
  return next(
1192
1484
  str(row[2]) for row in conn.execute("PRAGMA database_list")
@@ -1237,6 +1529,15 @@ def build_cached_codex_accounting(
1237
1529
  or clock regression goes cold. When the upper bound advances, paths with
1238
1530
  already-stored future rows crossing into the range are dirtied even though
1239
1531
  no mutation sequence moved.
1532
+
1533
+ #857: every successful call also returns — and records as the process's
1534
+ CONSUMED provenance (`codex_accounting_consumed_provenance`) — the
1535
+ population's provenance token, established here beside the state it names.
1536
+ A cold reconstruction, a ledger advance (whether or not it dirties a path)
1537
+ and a path replacement (a revealed future row included) each MINT a token
1538
+ no earlier population carried; only a capture that changed nothing — same
1539
+ ledger head, no dirty path, an upper bound that revealed no row — PRESERVES
1540
+ the state's token. A failed load publishes nothing, token included.
1240
1541
  """
1241
1542
  _assert_owner()
1242
1543
  if (
@@ -1246,7 +1547,7 @@ def build_cached_codex_accounting(
1246
1547
  ):
1247
1548
  raise ValueError("Codex accounting cache range must be aware and ordered")
1248
1549
  current_seq = _codex_accounting_mutation_seq(cache_conn)
1249
- database_path = _codex_main_database_path(cache_conn)
1550
+ database_path = codex_main_database_path(cache_conn)
1250
1551
  attribution_revision = _codex_window_attribution_revision(cache_conn)
1251
1552
  state = _CODEX_ACCOUNTING_CACHE_STATE
1252
1553
  prior_cached_entries = tuple(state.get("entries", ())) if state else ()
@@ -1341,6 +1642,8 @@ def build_cached_codex_accounting(
1341
1642
  }))
1342
1643
  generation = int(state.get("generation", 0)) + 1
1343
1644
  population_signature = signature(generation)
1645
+ # #857: a reconstruction is a new population, whatever it contains.
1646
+ provenance_token = _mint_codex_accounting_provenance()
1344
1647
  _CODEX_ACCOUNTING_CACHE_STATE.clear()
1345
1648
  _CODEX_ACCOUNTING_CACHE_STATE.update({
1346
1649
  "entries": entries,
@@ -1350,21 +1653,36 @@ def build_cached_codex_accounting(
1350
1653
  "extra": extra_signature,
1351
1654
  "generation": generation,
1352
1655
  "population_signature": population_signature,
1656
+ _CODEX_ACCOUNTING_PROVENANCE_STATE_KEY: provenance_token,
1353
1657
  })
1354
1658
  return CodexAccountingCacheResult(
1355
1659
  entries, (), accounts, True, prior_cached_entries, entries,
1356
1660
  population_signature=population_signature,
1661
+ provenance_token=_consume_codex_accounting_provenance(
1662
+ provenance_token),
1357
1663
  )
1358
1664
 
1359
1665
  prior_entries = tuple(state["entries"])
1360
1666
  if not dirty:
1361
1667
  population_signature = signature(int(state.get("generation", 0)))
1668
+ # #857: no row moved, but a ledger advance still consumed a ledger
1669
+ # state the population did not carry before, so it is renamed; only a
1670
+ # capture over the same head keeps the state's token.
1671
+ provenance_token = state.get(_CODEX_ACCOUNTING_PROVENANCE_STATE_KEY)
1672
+ if (
1673
+ current_seq != int(state.get("seq", -1))
1674
+ or not isinstance(provenance_token, int)
1675
+ ):
1676
+ provenance_token = _mint_codex_accounting_provenance()
1362
1677
  state["seq"] = current_seq
1363
1678
  state["end"] = range_end
1364
1679
  state["population_signature"] = population_signature
1680
+ state[_CODEX_ACCOUNTING_PROVENANCE_STATE_KEY] = provenance_token
1365
1681
  return CodexAccountingCacheResult(
1366
1682
  prior_entries, (), (), False,
1367
1683
  population_signature=population_signature,
1684
+ provenance_token=_consume_codex_accounting_provenance(
1685
+ provenance_token),
1368
1686
  )
1369
1687
 
1370
1688
  dirty_paths = tuple(sorted(dirty))
@@ -1405,14 +1723,19 @@ def build_cached_codex_accounting(
1405
1723
  _mark_snapshot_accelerators_dirty()
1406
1724
  generation = int(state.get("generation", 0)) + 1
1407
1725
  population_signature = signature(generation)
1726
+ # #857: a path replacement is a new population, even one whose rows
1727
+ # compare equal — the ledger moved or the bound revealed a row.
1728
+ provenance_token = _mint_codex_accounting_provenance()
1408
1729
  state["entries"] = entries
1409
1730
  state["seq"] = current_seq
1410
1731
  state["end"] = range_end
1411
1732
  state["generation"] = generation
1412
1733
  state["population_signature"] = population_signature
1734
+ state[_CODEX_ACCOUNTING_PROVENANCE_STATE_KEY] = provenance_token
1413
1735
  return CodexAccountingCacheResult(
1414
1736
  entries, dirty_paths, dirty_accounts, False, changed_old, changed_new,
1415
1737
  population_signature=population_signature,
1738
+ provenance_token=_consume_codex_accounting_provenance(provenance_token),
1416
1739
  )
1417
1740
 
1418
1741