cctally 1.116.0 → 1.117.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 (72) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +4 -6
  3. package/bin/_cctally_account.py +57 -3
  4. package/bin/_cctally_cache.py +5720 -885
  5. package/bin/_cctally_claude_attribution.py +997 -0
  6. package/bin/_cctally_completion.py +101 -28
  7. package/bin/_cctally_core.py +235 -12
  8. package/bin/_cctally_dashboard.py +224 -7
  9. package/bin/_cctally_dashboard_conversation.py +107 -21
  10. package/bin/_cctally_dashboard_envelope.py +34 -0
  11. package/bin/_cctally_db.py +928 -80
  12. package/bin/_cctally_diagnosis_sources.py +78 -13
  13. package/bin/_cctally_doctor.py +211 -86
  14. package/bin/_cctally_five_hour.py +389 -18
  15. package/bin/_cctally_forecast.py +30 -6
  16. package/bin/_cctally_journal.py +2418 -158
  17. package/bin/_cctally_oauth_samples.py +185 -7
  18. package/bin/_cctally_parser.py +38 -0
  19. package/bin/_cctally_project.py +34 -8
  20. package/bin/_cctally_quarantine.py +821 -343
  21. package/bin/_cctally_quota.py +69 -4
  22. package/bin/_cctally_quota_calibration.py +677 -3
  23. package/bin/_cctally_quota_model.py +927 -127
  24. package/bin/_cctally_record.py +922 -445
  25. package/bin/_cctally_rederive.py +119 -15
  26. package/bin/_cctally_statusline.py +1048 -176
  27. package/bin/_cctally_store.py +544 -110
  28. package/bin/_cctally_tui.py +93 -9
  29. package/bin/_cctally_update.py +77 -14
  30. package/bin/_lib_account_protocol.py +227 -19
  31. package/bin/_lib_claude_attribution.py +421 -0
  32. package/bin/_lib_clone_copy.py +30 -0
  33. package/bin/_lib_codex_conversation_query.py +45 -9
  34. package/bin/_lib_codex_conversation_watch.py +72 -13
  35. package/bin/_lib_codex_rollout_container.py +234 -19
  36. package/bin/_lib_codex_worker_lifecycle.py +374 -0
  37. package/bin/_lib_credit.py +22 -13
  38. package/bin/_lib_dashboard_sources.py +163 -10
  39. package/bin/_lib_detector_access_registry.py +1335 -166
  40. package/bin/_lib_doctor.py +276 -92
  41. package/bin/_lib_ingest_frontier.py +269 -36
  42. package/bin/_lib_journal.py +273 -22
  43. package/bin/_lib_jsonl.py +12 -0
  44. package/bin/_lib_meter_rate_change.py +72 -0
  45. package/bin/_lib_outcome_quarantine.py +917 -554
  46. package/bin/_lib_provider_activity.py +24 -6
  47. package/bin/_lib_quota_calibration.py +57 -3
  48. package/bin/_lib_quota_copy.py +7 -2
  49. package/bin/_lib_quota_lineage.py +613 -0
  50. package/bin/_lib_quota_model.py +435 -22
  51. package/bin/_lib_rate_change_delivery.py +16 -0
  52. package/bin/_lib_reclaim_planner.py +6 -4
  53. package/bin/_lib_rederive.py +2046 -21
  54. package/bin/_lib_selector_state.py +131 -3
  55. package/bin/_lib_stale_proof.py +113 -4
  56. package/bin/_lib_statusline.py +72 -3
  57. package/bin/_lib_statusline_candidates.py +246 -42
  58. package/bin/_lib_write_io.py +418 -5
  59. package/bin/cctally +24 -11
  60. package/dashboard/static/assets/{ConversationsView-Dflb_myA.js → ConversationsView-Dcfn8T6f.js} +1 -1
  61. package/dashboard/static/assets/{DoctorModal-DFX5CfIs.js → DoctorModal-BqmXbnhn.js} +1 -1
  62. package/dashboard/static/assets/{ModalRoot-HlxoagIa.js → ModalRoot-Ck3WJK2V.js} +1 -1
  63. package/dashboard/static/assets/{ProjectsDrillPanel-C3PJy1xx.js → ProjectsDrillPanel-B-HZkDnT.js} +1 -1
  64. package/dashboard/static/assets/{SourceDetailModal-BczsWKhD.js → SourceDetailModal-QhJkHB7i.js} +1 -1
  65. package/dashboard/static/assets/{UpdateModal-Os-Lo5cs.js → UpdateModal-CkXEIQcK.js} +1 -1
  66. package/dashboard/static/assets/index-D34eYhAc.css +1 -0
  67. package/dashboard/static/assets/{index-CYZ3NLqF.js → index-HKS1s56o.js} +3 -3
  68. package/dashboard/static/assets/{outlineNavigation-k-zIif6W.js → outlineNavigation-BNGaNPCp.js} +1 -1
  69. package/dashboard/static/assets/{useKeymap-CW7Up0Ay.js → useKeymap-CUgEnnua.js} +1 -1
  70. package/dashboard/static/dashboard.html +3 -3
  71. package/package.json +5 -1
  72. package/dashboard/static/assets/index-BKITk1CT.css +0 -1
@@ -50,7 +50,15 @@ current decision can be appended concurrently). It
50
50
  identical decision → nothing; only an excluded legacy decision, or an
51
51
  admitted decision the gap changes → one completed correction batch whose
52
52
  action is above every retained revision of the id; an admitted decision the
53
- gap removes → a tombstone in that batch;
53
+ gap removes → a tombstone in that batch. Closed five-hour money stays
54
+ frozen (#1040): for every block the gap touches that holds a closed close,
55
+ `_lib_rederive.plan_completion_five_hour` decides the close and milestone
56
+ records instead. A close keeps its recorded money group unless the gap
57
+ changes the block's entry population (or a recorded missing card is
58
+ corrected), and such a change lands only paired with its milestones in the
59
+ same batch; every candidate is checked against the #884 milestone lineage,
60
+ and none that makes a milestone's decision worse than the gap's required
61
+ additions alone is appended;
54
62
  5. rebuilds the live index over the result. The consumed-input frontier stays
55
63
  distinct from output positions: inputs another process appended between
56
64
  the window and the appended outputs are completed in a further round, and
@@ -893,6 +901,9 @@ def _apply_owed_outputs(base, start, output_end, boundary) -> frozenset:
893
901
  backfill(conn, only_missing=True,
894
902
  journaled_first=(
895
903
  _journal.journaled_block_first_observations(
904
+ prefix.slots, selection.by_id)),
905
+ retired_openings=(
906
+ _journal.retired_block_openings(
896
907
  prefix.slots, selection.by_id)))
897
908
  conn.execute("BEGIN IMMEDIATE")
898
909
  try:
@@ -1246,8 +1257,16 @@ def _complete_window(base, tmp, start, target, boundary, round_index, *,
1246
1257
  if record is not None:
1247
1258
  _journal._normalize_legacy_account_stamp(
1248
1259
  record, prefix.cutover_claude)
1260
+ accumulators: dict = {}
1249
1261
  selection = _lib_journal.resolve_effective_events(
1250
- prefix.slots, protocol_prefix_evidence=tuple(prefix.evidence))
1262
+ prefix.slots, protocol_prefix_evidence=tuple(prefix.evidence),
1263
+ accumulators=accumulators)
1264
+ # #1040 (spec §5.1): the #884 lineage, built exactly as `db rederive`
1265
+ # builds it; the fold is released before the replays.
1266
+ lineage = _lib_rederive.build_five_hour_lineage(
1267
+ accumulators.pop("fold"), selection=selection,
1268
+ physical_revisions=prefix.excluded_revisions)
1269
+ del accumulators
1251
1270
  # Inert (incomplete or tainted) batch actions of OTHER families still
1252
1271
  # occupy their revision: a later retry of that batch must not collide.
1253
1272
  for record in prefix.slots:
@@ -1288,45 +1307,94 @@ def _complete_window(base, tmp, start, target, boundary, round_index, *,
1288
1307
  with_gap = _owned_by_id(events_b)
1289
1308
  without_gap = _owned_by_id(events_a)
1290
1309
 
1310
+ def physical_of(event_id):
1311
+ selected = selection.by_id.get(event_id)
1312
+ return [rev for rev in (
1313
+ prefix.excluded_revisions.get(event_id),
1314
+ prefix.inert_revisions.get(event_id),
1315
+ None if selected is None else selected.rev,
1316
+ ) if rev is not None]
1317
+
1318
+ def current_of(event_id):
1319
+ selected = selection.by_id.get(event_id)
1320
+ return (selected.record if selected is not None
1321
+ and selected.status == "active" else None)
1322
+
1323
+ acted = frozenset(
1324
+ event_id for event_id in set(with_gap) | set(without_gap)
1325
+ if not (event_id in with_gap and event_id in without_gap
1326
+ and _lib_rederive._semantic_event(with_gap[event_id])
1327
+ == _lib_rederive._semantic_event(without_gap[event_id])))
1328
+ retirements = []
1329
+ for event_id in sorted(acted):
1330
+ current = current_of(event_id)
1331
+ if (event_id not in with_gap and current is not None
1332
+ and (current.get("payload") or {}).get("kind")
1333
+ in _lib_rederive._LINEAGE_KINDS):
1334
+ retirements.append({"action": "tombstone", "id": event_id,
1335
+ "rev": max(physical_of(event_id)) + 1,
1336
+ "at": str(current["at"]), "payload": None})
1337
+ five_hour_current = {
1338
+ event_id: item.record for event_id, item in selection.by_id.items()
1339
+ if item.status == "active" and item.record is not None
1340
+ and (item.record.get("payload") or {}).get("kind")
1341
+ in _lib_rederive._LINEAGE_KINDS}
1342
+ physical = {}
1343
+ for event_id in set(five_hour_current) | set(with_gap) | set(without_gap):
1344
+ revisions = physical_of(event_id)
1345
+ if revisions:
1346
+ physical[event_id] = max(revisions)
1347
+ # #1040 (spec §5): closed five-hour money stays frozen. The planner owns
1348
+ # every five-hour id of a touched non-OPEN block; today's per-id path
1349
+ # keeps every other id, retirements included.
1350
+ plan = _lib_rederive.plan_completion_five_hour(
1351
+ current=five_hour_current, with_gap=with_gap,
1352
+ without_gap=without_gap, acted=acted, physical=physical,
1353
+ selection=selection, lineage=lineage, retirements=retirements)
1354
+ del lineage, five_hour_current
1355
+
1291
1356
  plain: list = []
1292
1357
  actions: list = []
1293
- for event_id in sorted(set(with_gap) | set(without_gap)):
1358
+
1359
+ def append_desired(event_id, desired):
1360
+ """The binding append policy for one desired record (spec §5.5)."""
1361
+ current = current_of(event_id)
1362
+ if (current is not None and _lib_rederive._semantic_event(current)
1363
+ == _lib_rederive._semantic_event(desired)):
1364
+ return # an admitted identical decision: nothing
1365
+ physical = physical_of(event_id)
1366
+ if not physical:
1367
+ plain.append(desired)
1368
+ return
1369
+ actions.append({"action": "replace", "id": event_id,
1370
+ "rev": max(physical) + 1,
1371
+ "at": str(desired["at"]),
1372
+ "payload": dict(desired.get("payload") or {})})
1373
+
1374
+ for event_id in sorted(set(with_gap) | set(without_gap)
1375
+ | set(plan.targets)):
1376
+ if event_id in plan.targets:
1377
+ append_desired(event_id, plan.targets[event_id])
1378
+ continue
1379
+ if event_id in plan.handled:
1380
+ continue
1294
1381
  desired = with_gap.get(event_id)
1295
- reference = without_gap.get(event_id)
1296
- if (desired is not None and reference is not None
1297
- and _lib_rederive._semantic_event(desired)
1298
- == _lib_rederive._semantic_event(reference)):
1382
+ if event_id not in acted:
1299
1383
  # Not caused by the gap: an unrelated difference against the
1300
1384
  # journal (a repricing, a previous release's input the current
1301
1385
  # pipeline extends) is never acted on.
1302
1386
  continue
1303
- selected = selection.by_id.get(event_id)
1304
- current = (selected.record if selected is not None
1305
- and selected.status == "active" else None)
1306
- physical = [rev for rev in (
1307
- prefix.excluded_revisions.get(event_id),
1308
- prefix.inert_revisions.get(event_id),
1309
- None if selected is None else selected.rev,
1310
- ) if rev is not None]
1387
+ current = current_of(event_id)
1311
1388
  if desired is None:
1312
1389
  if current is not None:
1313
1390
  actions.append({"action": "tombstone", "id": event_id,
1314
- "rev": max(physical) + 1,
1391
+ "rev": max(physical_of(event_id)) + 1,
1315
1392
  "at": str(current["at"]), "payload": None})
1316
1393
  continue
1317
1394
  if current is not None:
1318
1395
  desired = _lib_rederive._preserve_non_derivable_state(
1319
1396
  current, desired)
1320
- if (_lib_rederive._semantic_event(current)
1321
- == _lib_rederive._semantic_event(desired)):
1322
- continue # an admitted identical decision: nothing
1323
- if not physical:
1324
- plain.append(desired)
1325
- continue
1326
- actions.append({"action": "replace", "id": event_id,
1327
- "rev": max(physical) + 1,
1328
- "at": str(desired["at"]),
1329
- "payload": dict(desired.get("payload") or {})})
1397
+ append_desired(event_id, desired)
1330
1398
 
1331
1399
  records: list = []
1332
1400
  if actions:
@@ -1344,8 +1412,13 @@ def _complete_window(base, tmp, start, target, boundary, round_index, *,
1344
1412
  # are byte-identical and the selector keeps each once.
1345
1413
  at=str(prefix.begin_at.get(batch_id) or _iso_now()),
1346
1414
  actions=actions))
1347
- order = {id(event): index for index, event in enumerate(events_b)}
1348
- plain.sort(key=lambda event: order.get(id(event), 0))
1415
+ # Derivation order of each id's first with-gap candidate (the one
1416
+ # `_owned_by_id` keeps), so a planner target sorts where its id was
1417
+ # derived, exactly like the candidate it stands for.
1418
+ order: dict = {}
1419
+ for index, event in enumerate(events_b):
1420
+ order.setdefault(event.get("id"), index)
1421
+ plain.sort(key=lambda event: order.get(event.get("id"), 0))
1349
1422
  records.extend(plain)
1350
1423
  if not records:
1351
1424
  return False
@@ -79,6 +79,7 @@ def _init_paths_from_env() -> None:
79
79
  global STATUSLINE_ACCOUNT_BOUND_DIR, LEGACY_SPOOL_BASELINE_PATH
80
80
  global ACCOUNT_BOUND_CUTOVER_CACHE_PATH, LEGACY_WRITER_EVIDENCE_PATH
81
81
  global STATUSLINE_ACCOUNT_BOUND_V1_DIR, ACCOUNT_BOUND_REVISION_CACHE_PATH
82
+ global STATUSLINE_ACCOUNT_BOUND_V2_DIR, ACCOUNT_BOUND_REVISION3_CACHE_PATH
82
83
  global OAUTH_BACKOFF_MARKER_PATH, OAUTH_BACKOFF_COUNT_PATH
83
84
  global UPDATE_STATE_PATH, UPDATE_SUPPRESS_PATH
84
85
  global UPDATE_LOCK_PATH, UPDATE_LOG_PATH, UPDATE_LOG_ROTATED_PATH
@@ -202,8 +203,15 @@ def _init_paths_from_env() -> None:
202
203
  # `accounts/` tree and `transport.last` join the legacy spool inventory
203
204
  # (`_cctally_journal._legacy_spool_paths`), and this binary never writes,
204
205
  # prunes or deletes anything under it.
206
+ #
207
+ # #1019/#1014 (spec 4.7 C4): revision 3 moves every current coordination
208
+ # file again, to the v3 root, for the same reason; no v2 state is
209
+ # imported, the v2 root's `accounts/` tree and `transport.last` join the
210
+ # legacy spool inventory, and this binary never writes, prunes or deletes
211
+ # anything under v2 (or v1).
205
212
  STATUSLINE_ACCOUNT_BOUND_V1_DIR = APP_DIR / "statusline-account-bound-v1"
206
- STATUSLINE_ACCOUNT_BOUND_DIR = APP_DIR / "statusline-account-bound-v2"
213
+ STATUSLINE_ACCOUNT_BOUND_V2_DIR = APP_DIR / "statusline-account-bound-v2"
214
+ STATUSLINE_ACCOUNT_BOUND_DIR = APP_DIR / "statusline-account-bound-v3"
207
215
  LEGACY_SPOOL_BASELINE_PATH = (
208
216
  STATUSLINE_ACCOUNT_BOUND_DIR / "legacy-spool-baseline.json")
209
217
  ACCOUNT_BOUND_CUTOVER_CACHE_PATH = (
@@ -212,6 +220,10 @@ def _init_paths_from_env() -> None:
212
220
  #: accelerates the lookup and never confers authority.
213
221
  ACCOUNT_BOUND_REVISION_CACHE_PATH = (
214
222
  STATUSLINE_ACCOUNT_BOUND_DIR / "revision-coordinate.json")
223
+ #: The revision-3 boundary's coordinate cache (#1019/#1014), likewise an
224
+ #: accelerator only.
225
+ ACCOUNT_BOUND_REVISION3_CACHE_PATH = (
226
+ STATUSLINE_ACCOUNT_BOUND_DIR / "revision3-coordinate.json")
215
227
  LEGACY_WRITER_EVIDENCE_PATH = (
216
228
  STATUSLINE_ACCOUNT_BOUND_DIR / "legacy-writer-evidence.json")
217
229
  OAUTH_BACKOFF_MARKER_PATH = APP_DIR / "oauth-backoff.until"
@@ -554,7 +566,40 @@ _init_paths_from_env()
554
566
  # `baseline_generation`, `pending_generation`, `pending_position`) that the
555
567
  # five-hour credit generation (Task 5) writes. Both projections are rebuilt
556
568
  # from the journal; the confirmation state stays disposable.
557
- STATS_INDEX_EPOCH = 1019
569
+ # 1019 -> 1020 (#1019/#1014 spec 4.7 C6): revision 3 of the fence changes the
570
+ # authoritative interpretation once more — after the
571
+ # `account_bound_revision_v3` boundary a Claude-scoped record without
572
+ # `wpx == 3` is excluded — so an index, a derivation-completeness proof or a
573
+ # selector continuation produced under revision 2 is not current. No
574
+ # numbered migration and no accounting column; the rebuild runs under #997's
575
+ # bounded profile.
576
+ # 1020 -> 1021 (#943/#956 spec 4.7): a five-hour credit's exact boundary.
577
+ # `five_hour_credit_confirmation_state` gains `pending_captured_at_utc` (the
578
+ # arming observation's capture second, recorded by both detectors) and the
579
+ # new harvest family `five_hour_credit_boundaries` (`fhcb`, order 29) retains
580
+ # that second beside the credit, whose floored slot stays its identity. Every
581
+ # post-credit read compares against the resolved boundary, so an index
582
+ # materialized under the floored band is not current. No numbered migration;
583
+ # `DERIVATION_FRONTIER_EPOCH` is unchanged.
584
+ # The same epoch also covers #956 class 2 (spec 4.6): every five-hour block
585
+ # creation retains an opening fact in `five_hour_block_openings` (`fhbo`,
586
+ # order 35), whose fold materializes the open block, and open-block end
587
+ # columns come from one capture-order projection.
588
+ # 1021 -> 1022 (#1015 spec 5.1, 5.7): correction-to-fit lineage. New
589
+ # journal-truth projections `claude_weekly_observation_index`,
590
+ # `claude_weekly_correction_history`, `claude_weekly_correction_ops`,
591
+ # `correction_batch_lineage` and `correction_batch_footprint`, the
592
+ # `meter_rate_change_supersessions` table and the NULLABLE
593
+ # `meter_rate_change_events.fit_lineage` column, so an index built without
594
+ # them is not current. No numbered migration; `DERIVATION_FRONTIER_EPOCH` is
595
+ # unchanged.
596
+ # 1022 -> 1023 (#948 spec 6.11): per-axis automatic recovery from the
597
+ # outcome quarantine. `outcome_quarantine_envelope` and
598
+ # `outcome_quarantine_certificates` are replaced by
599
+ # `outcome_quarantine_episodes`, `outcome_quarantine_bounds` and
600
+ # `outcome_quarantine_axis_state`, so an index built without them is not
601
+ # current. No numbered migration; `DERIVATION_FRONTIER_EPOCH` is unchanged.
602
+ STATS_INDEX_EPOCH = 1023
558
603
  LEGACY_STATS_HEAD = 13
559
604
 
560
605
 
@@ -1870,11 +1915,29 @@ def _apply_quota_projection_schema(conn: sqlite3.Connection) -> None:
1870
1915
  detector_input_causes TEXT,
1871
1916
  composition_provenance TEXT,
1872
1917
  baseline_withheld_days INTEGER,
1918
+ fit_lineage TEXT,
1873
1919
  UNIQUE(provider, account_key, effective_from)
1874
1920
  );
1875
1921
  CREATE INDEX IF NOT EXISTS idx_meter_rate_change_events_key
1876
1922
  ON meter_rate_change_events(provider, account_key, effective_from);
1877
1923
 
1924
+ -- #1015 epoch 1022 (spec 5.7). `fit_lineage` above is the canonical
1925
+ -- JSON lineage of the fits a transition compared, NULL on a row
1926
+ -- detected before #1015. `meter_rate_change_supersessions` is the
1927
+ -- terminal, idempotent projection of the journalled
1928
+ -- `meter_rate_change_supersession` event: one row per superseded
1929
+ -- transition identity, `corrections` the canonical JSON op ids.
1930
+ CREATE TABLE IF NOT EXISTS meter_rate_change_supersessions (
1931
+ provider TEXT NOT NULL,
1932
+ account_key TEXT NOT NULL,
1933
+ effective_from TEXT NOT NULL,
1934
+ reason TEXT NOT NULL,
1935
+ corrections TEXT NOT NULL,
1936
+ superseded_at_utc TEXT NOT NULL,
1937
+ created_at_utc TEXT NOT NULL,
1938
+ PRIMARY KEY (provider, account_key, effective_from)
1939
+ );
1940
+
1878
1941
  CREATE TABLE IF NOT EXISTS quota_alert_arming (
1879
1942
  id INTEGER PRIMARY KEY AUTOINCREMENT,
1880
1943
  source TEXT NOT NULL,
@@ -2747,6 +2810,7 @@ def open_db(*, _target_path=None) -> sqlite3.Connection:
2747
2810
  baseline_generation TEXT,
2748
2811
  pending_generation TEXT,
2749
2812
  pending_position TEXT,
2813
+ pending_captured_at_utc TEXT,
2750
2814
  PRIMARY KEY (account_key, five_hour_window_key, source)
2751
2815
  )
2752
2816
  """
@@ -2802,6 +2866,78 @@ def open_db(*, _target_path=None) -> sqlite3.Connection:
2802
2866
  )
2803
2867
  """
2804
2868
  )
2869
+ # #1015 epoch 1022 (spec 5.1): correction-to-fit lineage facts, all
2870
+ # journal-truth projections filled identically by live ingest, the
2871
+ # scratch replays and both apply-only rebuilds.
2872
+ #
2873
+ # `claude_weekly_observation_index`: one row per admitted Claude usage
2874
+ # observation carrying a weekly reading, projected before snapshot
2875
+ # deduplication, the five-hour adjustment, weekly suppression and
2876
+ # credit detection, never pruned. `account_key` is the account live
2877
+ # ingest reads after the cutover-aware legacy normalization;
2878
+ # `captured_at_utc` is canonical full-precision UTC.
2879
+ conn.execute(
2880
+ """
2881
+ CREATE TABLE IF NOT EXISTS claude_weekly_observation_index (
2882
+ observation_id TEXT PRIMARY KEY,
2883
+ account_key TEXT NOT NULL,
2884
+ captured_at_utc TEXT NOT NULL
2885
+ )
2886
+ """
2887
+ )
2888
+ conn.execute(
2889
+ "CREATE INDEX IF NOT EXISTS idx_claude_weekly_observation_index_window "
2890
+ "ON claude_weekly_observation_index (account_key, captured_at_utc)"
2891
+ )
2892
+ # Every decision of every reviewed weekly op (not only the latest),
2893
+ # with the DECODED disposition: a `weekly_axis_retractions` member is
2894
+ # `retract`. Idempotent under the primary key.
2895
+ conn.execute(
2896
+ """
2897
+ CREATE TABLE IF NOT EXISTS claude_weekly_correction_history (
2898
+ decision_op_id TEXT NOT NULL,
2899
+ observation_id TEXT NOT NULL,
2900
+ disposition TEXT NOT NULL CHECK(disposition IN ('hold','retract','accept')),
2901
+ PRIMARY KEY (decision_op_id, observation_id)
2902
+ )
2903
+ """
2904
+ )
2905
+ # Where each reviewed op sits in the journal: the END coordinate of
2906
+ # its line, comparable through `journal_coordinate_key`. Scratch
2907
+ # replays never write it.
2908
+ conn.execute(
2909
+ """
2910
+ CREATE TABLE IF NOT EXISTS claude_weekly_correction_ops (
2911
+ decision_op_id TEXT PRIMARY KEY,
2912
+ op_segment TEXT NOT NULL,
2913
+ op_offset INTEGER NOT NULL
2914
+ )
2915
+ """
2916
+ )
2917
+ # Completed correction batches (a row means completed), at the END
2918
+ # coordinate of the commit marker, and the `(provider, account_key)`
2919
+ # pairs each one touched (`_lib_quota_lineage.batch_footprint`); an
2920
+ # empty batch has no footprint rows, `('*', '*')` only when ownership
2921
+ # is genuinely unknowable. Owned by lineage, never by the selector.
2922
+ conn.execute(
2923
+ """
2924
+ CREATE TABLE IF NOT EXISTS correction_batch_lineage (
2925
+ batch_id TEXT PRIMARY KEY,
2926
+ commit_segment TEXT NOT NULL,
2927
+ commit_offset INTEGER NOT NULL
2928
+ )
2929
+ """
2930
+ )
2931
+ conn.execute(
2932
+ """
2933
+ CREATE TABLE IF NOT EXISTS correction_batch_footprint (
2934
+ batch_id TEXT NOT NULL,
2935
+ provider TEXT NOT NULL,
2936
+ account_key TEXT NOT NULL,
2937
+ PRIMARY KEY (batch_id, provider, account_key)
2938
+ )
2939
+ """
2940
+ )
2805
2941
  _backfill_week_reset_events(conn)
2806
2942
 
2807
2943
  # ── five_hour_reset_events (Anthropic-issued in-place 5h credits) ──
@@ -2845,6 +2981,64 @@ def open_db(*, _target_path=None) -> sqlite3.Connection:
2845
2981
  conn, "five_hour_reset_events", "account_key",
2846
2982
  "TEXT NOT NULL DEFAULT 'unattributed'")
2847
2983
 
2984
+ # ── five_hour_credit_boundaries (#943 spec 4.2.2, epoch 1021) ──
2985
+ # The exact boundary of one five-hour credit: the arming
2986
+ # observation's capture second. A companion of the credit keyed by
2987
+ # its natural key (`effective_reset_at_utc` is the credit's floored
2988
+ # slot, a link without a formal FK) and bound to its content through
2989
+ # `credit_revision`; the boundary resolver
2990
+ # (`_cctally_record.resolve_five_hour_reset_event`) uses it only when
2991
+ # that revision matches the credit row's. Harvested as
2992
+ # `five_hour_credit_boundary` (`fhcb`, order 29), before its credit.
2993
+ conn.execute(
2994
+ """
2995
+ CREATE TABLE IF NOT EXISTS five_hour_credit_boundaries (
2996
+ id INTEGER PRIMARY KEY,
2997
+ account_key TEXT NOT NULL,
2998
+ five_hour_window_key INTEGER NOT NULL,
2999
+ effective_reset_at_utc TEXT NOT NULL,
3000
+ boundary_at_utc TEXT NOT NULL,
3001
+ arming_observation_id TEXT NOT NULL,
3002
+ credit_revision TEXT NOT NULL,
3003
+ detected_at_utc TEXT NOT NULL,
3004
+ journal_id TEXT,
3005
+ UNIQUE(account_key, five_hour_window_key, effective_reset_at_utc)
3006
+ )
3007
+ """
3008
+ )
3009
+
3010
+ # ── five_hour_block_openings (#956 spec 4.6.1, epoch 1021) ──
3011
+ # The opening fact of one five-hour block: the values the creating
3012
+ # upsert wrote (start columns, the fold-effective opening five-hour and
3013
+ # weekly readings with their held provenance, `created_at_utc` as
3014
+ # written) and the journal id of the observation or op that created
3015
+ # it. One row per block, inserted only by the live writer at creation
3016
+ # (`_cctally_record.maybe_update_five_hour_block`) and immutable.
3017
+ # Harvested as `five_hour_block_open` (`fhbo`, order 35); its fold
3018
+ # materializes the open block, so an apply-only replay reproduces a
3019
+ # block no surviving snapshot names.
3020
+ conn.execute(
3021
+ """
3022
+ CREATE TABLE IF NOT EXISTS five_hour_block_openings (
3023
+ id INTEGER PRIMARY KEY,
3024
+ account_key TEXT NOT NULL,
3025
+ five_hour_window_key INTEGER NOT NULL,
3026
+ five_hour_resets_at TEXT NOT NULL,
3027
+ block_start_at TEXT NOT NULL,
3028
+ first_observed_at_utc TEXT NOT NULL,
3029
+ created_at_utc TEXT NOT NULL,
3030
+ opening_record_id TEXT NOT NULL,
3031
+ creation_kind TEXT NOT NULL,
3032
+ five_hour_percent REAL NOT NULL,
3033
+ seven_day_pct_at_block_start REAL,
3034
+ weekly_observation_held INTEGER NOT NULL DEFAULT 0,
3035
+ weekly_basis_captured_at_utc TEXT,
3036
+ journal_id TEXT,
3037
+ UNIQUE(account_key, five_hour_window_key)
3038
+ )
3039
+ """
3040
+ )
3041
+
2848
3042
  # ── five_hour_blocks (rollup, one row per API-anchored 5h block) ──
2849
3043
  conn.execute(
2850
3044
  """
@@ -3312,6 +3506,7 @@ def open_db(*, _target_path=None) -> sqlite3.Connection:
3312
3506
  "five_hour_reset_events", "five_hour_blocks", "weekly_credit_floors",
3313
3507
  "percent_milestones", "five_hour_milestones", "budget_milestones",
3314
3508
  "projected_milestones", "project_budget_milestones",
3509
+ "five_hour_credit_boundaries", "five_hour_block_openings",
3315
3510
  ):
3316
3511
  add_column_if_missing(conn, _jtable, "journal_id", "TEXT")
3317
3512
  conn.execute(
@@ -3323,13 +3518,14 @@ def open_db(*, _target_path=None) -> sqlite3.Connection:
3323
3518
  # inserted this cycle (`WHERE journal_id IS NULL`). A partial index over
3324
3519
  # exactly those un-stamped rows keeps that scan O(this-cycle inserts), not
3325
3520
  # O(table) — at the 10x envelope the stamped rows are ~all of the table, so
3326
- # a full scan would be pathological. Only the 8 HARVEST families need it
3521
+ # a full scan would be pathological. Only the 10 HARVEST families need it
3327
3522
  # (the Model-A / op-fold tables — weekly_usage_snapshots, weekly_cost_
3328
3523
  # snapshots, weekly_credit_floors — are never harvest-scanned).
3329
3524
  for _htable in (
3330
3525
  "week_reset_events", "five_hour_reset_events", "five_hour_blocks",
3331
3526
  "percent_milestones", "five_hour_milestones", "budget_milestones",
3332
3527
  "projected_milestones", "project_budget_milestones",
3528
+ "five_hour_credit_boundaries", "five_hour_block_openings",
3333
3529
  ):
3334
3530
  conn.execute(
3335
3531
  f"CREATE INDEX IF NOT EXISTS idx_{_htable}_journal_id_null "
@@ -3531,18 +3727,45 @@ def open_db(*, _target_path=None) -> sqlite3.Connection:
3531
3727
  "reason TEXT NOT NULL, "
3532
3728
  "at TEXT)"
3533
3729
  )
3730
+ # #948 epoch 1023 (spec 6.11): the per-axis recovery barrier replaces
3731
+ # the envelope and the certificates. Incomplete-inventory episodes by
3732
+ # the coordinate of their opener; the per-scope, per-axis bounds
3733
+ # summaries (plus the generation's cohort and expiry under scope "*",
3734
+ # axis "meta", and each account's latest control witness under axis
3735
+ # "control"); and each (generation, account, axis)'s recovery state.
3736
+ conn.execute(
3737
+ "CREATE TABLE IF NOT EXISTS outcome_quarantine_episodes ("
3738
+ "opener_segment TEXT, "
3739
+ "opener_offset INTEGER, "
3740
+ "opened_at TEXT, "
3741
+ "closer_segment TEXT, "
3742
+ "closer_offset INTEGER, "
3743
+ "closed_at TEXT, "
3744
+ "PRIMARY KEY (opener_segment, opener_offset))"
3745
+ )
3534
3746
  conn.execute(
3535
- "CREATE TABLE IF NOT EXISTS outcome_quarantine_envelope ("
3536
- "scope TEXT PRIMARY KEY, "
3537
- "envelope_json TEXT NOT NULL)"
3747
+ "CREATE TABLE IF NOT EXISTS outcome_quarantine_bounds ("
3748
+ "scope TEXT NOT NULL, "
3749
+ "axis TEXT NOT NULL, "
3750
+ "bounds_json TEXT NOT NULL, "
3751
+ "PRIMARY KEY (scope, axis))"
3538
3752
  )
3539
3753
  conn.execute(
3540
- "CREATE TABLE IF NOT EXISTS outcome_quarantine_certificates ("
3541
- "account_key TEXT PRIMARY KEY, "
3542
- "accounting_certificate_json TEXT, "
3543
- "accounting_trigger_seq INTEGER, "
3544
- "control_certificate_json TEXT, "
3545
- "control_trigger_seq INTEGER)"
3754
+ "CREATE TABLE IF NOT EXISTS outcome_quarantine_axis_state ("
3755
+ "generation INTEGER NOT NULL, "
3756
+ "account_key TEXT NOT NULL, "
3757
+ "axis TEXT NOT NULL, "
3758
+ "state TEXT NOT NULL, "
3759
+ "waiting_for TEXT, "
3760
+ "coverage_id TEXT, "
3761
+ "witness_id TEXT, "
3762
+ "release_id TEXT, "
3763
+ "revocation TEXT, "
3764
+ "since TEXT, "
3765
+ "recovered_at TEXT, "
3766
+ "not_before INTEGER, "
3767
+ "witness_key TEXT, "
3768
+ "PRIMARY KEY (generation, account_key, axis))"
3546
3769
  )
3547
3770
  conn.execute(
3548
3771
  "CREATE TABLE IF NOT EXISTS journal_selector_batches ("