cctally 1.113.0 → 1.115.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 (97) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +4 -6
  3. package/bin/_cctally_account.py +32 -8
  4. package/bin/_cctally_cache.py +6035 -722
  5. package/bin/_cctally_completion.py +1287 -0
  6. package/bin/_cctally_config.py +37 -0
  7. package/bin/_cctally_core.py +400 -2
  8. package/bin/_cctally_dashboard.py +98 -20
  9. package/bin/_cctally_dashboard_conversation.py +292 -23
  10. package/bin/_cctally_dashboard_envelope.py +16 -3
  11. package/bin/_cctally_dashboard_perf.py +128 -9
  12. package/bin/_cctally_dashboard_sources.py +285 -213
  13. package/bin/_cctally_db.py +1177 -23
  14. package/bin/_cctally_diagnosis_sources.py +22 -4
  15. package/bin/_cctally_doctor.py +572 -45
  16. package/bin/_cctally_five_hour.py +84 -33
  17. package/bin/_cctally_forecast.py +108 -15
  18. package/bin/_cctally_journal.py +3950 -116
  19. package/bin/_cctally_journal_repair.py +101 -12
  20. package/bin/_cctally_milestone_history.py +82 -9
  21. package/bin/_cctally_parser.py +42 -0
  22. package/bin/_cctally_percent_breakdown.py +3 -2
  23. package/bin/_cctally_project.py +8 -2
  24. package/bin/_cctally_quarantine.py +652 -0
  25. package/bin/_cctally_quota.py +227 -37
  26. package/bin/_cctally_quota_model.py +360 -83
  27. package/bin/_cctally_record.py +691 -149
  28. package/bin/_cctally_rederive.py +320 -41
  29. package/bin/_cctally_refresh.py +84 -13
  30. package/bin/_cctally_source_analytics.py +83 -17
  31. package/bin/_cctally_statusline.py +6154 -517
  32. package/bin/_cctally_store.py +367 -53
  33. package/bin/_cctally_transcript.py +8 -7
  34. package/bin/_cctally_tui.py +61 -56
  35. package/bin/_cctally_weekrefs.py +128 -21
  36. package/bin/_lib_account_binding.py +152 -0
  37. package/bin/_lib_account_protocol.py +386 -0
  38. package/bin/_lib_aggregators.py +4 -0
  39. package/bin/_lib_cache_coverage.py +13 -1
  40. package/bin/_lib_cache_report.py +35 -6
  41. package/bin/_lib_cache_writer_lock.py +19 -3
  42. package/bin/_lib_clone_copy.py +142 -0
  43. package/bin/_lib_codex_conversation.py +32 -0
  44. package/bin/_lib_codex_conversation_query.py +297 -49
  45. package/bin/_lib_codex_conversation_watch.py +59 -14
  46. package/bin/_lib_codex_drain_admission.py +214 -0
  47. package/bin/_lib_codex_metadata.py +18 -1
  48. package/bin/_lib_codex_publication_barrier.py +277 -0
  49. package/bin/_lib_codex_rollout_container.py +502 -0
  50. package/bin/_lib_conversation_dispatch.py +3 -0
  51. package/bin/_lib_conversation_query.py +106 -59
  52. package/bin/_lib_conversation_retention.py +1605 -640
  53. package/bin/_lib_cost_provenance.py +53 -2
  54. package/bin/_lib_detector_access_registry.py +3817 -0
  55. package/bin/_lib_doctor.py +714 -69
  56. package/bin/_lib_five_hour.py +126 -0
  57. package/bin/_lib_fmt.py +11 -0
  58. package/bin/_lib_ingest_frontier.py +414 -20
  59. package/bin/_lib_journal.py +141 -10
  60. package/bin/_lib_jsonl.py +5 -4
  61. package/bin/_lib_legacy_spool.py +126 -0
  62. package/bin/_lib_legacy_writer_evidence.py +136 -0
  63. package/bin/_lib_merge_gate.py +231 -86
  64. package/bin/_lib_meter_rate_change.py +10 -0
  65. package/bin/_lib_outcome_quarantine.py +690 -0
  66. package/bin/_lib_perf.py +27 -1
  67. package/bin/_lib_pricing.py +199 -9
  68. package/bin/_lib_pricing_check.py +20 -4
  69. package/bin/_lib_quarantine.py +154 -0
  70. package/bin/_lib_quota_calibration.py +53 -8
  71. package/bin/_lib_quota_copy.py +2 -1
  72. package/bin/_lib_quota_model.py +288 -46
  73. package/bin/_lib_reclaim_planner.py +792 -0
  74. package/bin/_lib_rederive.py +319 -82
  75. package/bin/_lib_segment_summary.py +35 -2
  76. package/bin/_lib_snapshot_cache.py +21 -0
  77. package/bin/_lib_sqlite_close.py +81 -0
  78. package/bin/_lib_statusline_candidates.py +292 -23
  79. package/bin/_lib_subscription_weeks.py +100 -17
  80. package/bin/_lib_tick_stats.py +217 -17
  81. package/bin/_lib_wal_checkpoint.py +1077 -0
  82. package/bin/_lib_write_budget.py +483 -0
  83. package/bin/_lib_write_io.py +703 -0
  84. package/bin/cctally +31 -1
  85. package/dashboard/static/assets/{ConversationsView-CEZ-BUyi.js → ConversationsView-Bc8LCOAZ.js} +1 -1
  86. package/dashboard/static/assets/{DoctorModal-CE1oH_Zy.js → DoctorModal-D--qVBwe.js} +1 -1
  87. package/dashboard/static/assets/ModalRoot-DLPgyAM_.js +1 -0
  88. package/dashboard/static/assets/{ProjectsDrillPanel-B3DZoCeD.js → ProjectsDrillPanel-z53a4v4M.js} +1 -1
  89. package/dashboard/static/assets/{SourceDetailModal-CHp9EgLP.js → SourceDetailModal-Bw0T8W-W.js} +1 -1
  90. package/dashboard/static/assets/{UpdateModal-5iKST6jo.js → UpdateModal-D5AKx4Yw.js} +1 -1
  91. package/dashboard/static/assets/index-BKITk1CT.css +1 -0
  92. package/dashboard/static/assets/{index-Cm0VTxJC.js → index-BXDOzSng.js} +3 -3
  93. package/dashboard/static/assets/{outlineNavigation-DkXGL8J6.js → outlineNavigation-JObY7cUY.js} +1 -1
  94. package/dashboard/static/dashboard.html +2 -2
  95. package/package.json +19 -1
  96. package/dashboard/static/assets/ModalRoot-D5Y8IFUG.js +0 -1
  97. package/dashboard/static/assets/index-Dt4tqf43.css +0 -1
@@ -45,6 +45,7 @@ import sqlite3
45
45
  import sys
46
46
 
47
47
  import _lib_accounts # pure stdlib kernel; UNATTRIBUTED sentinel (#341)
48
+ from _lib_five_hour import latest_five_hour_reading
48
49
  from _cctally_core import _command_as_of, eprint, now_utc_iso, open_db, parse_iso_datetime
49
50
 
50
51
 
@@ -983,9 +984,10 @@ def _latest_seven_day_and_window(
983
984
  overlooked (#769 S11, #824). A `weekly_observation_held` row's five-hour
984
985
  fields are the writing tick's own, so it is the freshest statement of which
985
986
  window is current; excluding it would resolve an older window and report the
986
- live block closed. Rows with no ``five_hour_window_key`` are included here
987
- too, unchanged, because the latest row's key is the answer even when it is
988
- NULL.
987
+ live block closed. It is the key of the newest row that CARRIES a five-hour
988
+ reading (#825/#832 spec 4.7 revision 7, ``latest_five_hour_reading``): a
989
+ retired replica or a weekly-only reading can be the newest row, and its NULL
990
+ key would report the live block closed and leave its weekly end stale.
989
991
 
990
992
  **The weekly value is CREDIT-AWARE**, which is the #835 change. It comes from
991
993
  the most recent row that is not a post-floor stale replica — the same band
@@ -1065,8 +1067,15 @@ def _latest_seven_day_and_window(
1065
1067
  row = cur.fetchone()
1066
1068
  if row is None:
1067
1069
  return None, None
1068
- # Axis 1: the five-hour window key, from the latest row, held-inclusive.
1069
- key = row["five_hour_window_key"]
1070
+ # Axis 1: the five-hour window key, held-inclusive, from the newest
1071
+ # five-hour READING of the newest row's own account (#825/#832 spec
1072
+ # 4.7 revision 7, the one shared selection). The newest row can be a
1073
+ # retired replica or a weekly-only reading, which names no window.
1074
+ # Merged mode scopes the weekly walk to that account too, so an
1075
+ # account-blind selection here would pair another account's window
1076
+ # with this account's weekly value.
1077
+ reading = latest_five_hour_reading(conn, account_key=row["account_key"])
1078
+ key = None if reading is None else reading.window_key
1070
1079
  # Axis 2: the weekly value. The scope, the bands and the floor all come
1071
1080
  # from the NEWEST row and are resolved ONCE, because the walk may not
1072
1081
  # leave that scope — so there is nothing to memoize per candidate.
@@ -2689,10 +2698,10 @@ def cmd_five_hour_breakdown(args: argparse.Namespace) -> int:
2689
2698
  ])
2690
2699
  continue
2691
2700
  m = ev
2692
- cum = f"${m['blockCostUSD']:.6f}"
2701
+ cum = _c._fmt_usd_accounting(m['blockCostUSD'], 6)
2693
2702
  marg = (
2694
2703
  "n/a" if m["marginalCostUSD"] is None
2695
- else f"${m['marginalCostUSD']:.6f}"
2704
+ else _c._fmt_usd_accounting(m['marginalCostUSD'], 6)
2696
2705
  )
2697
2706
  # #834 S1 (#836): the EFFECTIVE weekly value, never the raw one. A
2698
2707
  # weekly-clamped tick stored a reading no reader ever saw, and this
@@ -2718,6 +2727,7 @@ def cmd_five_hour_breakdown(args: argparse.Namespace) -> int:
2718
2727
 
2719
2728
  def _backfill_five_hour_blocks(
2720
2729
  conn: sqlite3.Connection, *, only_missing: bool = False,
2730
+ journaled_first: "dict | None" = None,
2721
2731
  ) -> int:
2722
2732
  """One-shot historical backfill of five_hour_blocks from existing
2723
2733
  weekly_usage_snapshots data. Idempotent via UNIQUE(five_hour_window_key)
@@ -2740,9 +2750,58 @@ def _backfill_five_hour_blocks(
2740
2750
  re-materializes only the open block (block-only, no milestones — the
2741
2751
  journaled 5h-milestone evts carry their own values and resolve block_id
2742
2752
  against this row), keeping the pass O(open windows) instead of O(all blocks).
2753
+
2754
+ #825/#832 fix round J4 (#954): the rebuild projection reproduces the open
2755
+ block live ingest left, on its accounting columns too.
2756
+
2757
+ * ``journaled_first`` maps ``(account_key, five_hour_window_key)`` to the
2758
+ window's FIRST WRITER — its first effective `snapshot_accept` decision
2759
+ in journal order (`_cctally_journal.
2760
+ journaled_block_first_observations`) — and supplies the block's
2761
+ immutable start values (`first_observed_at_utc`,
2762
+ `five_hour_resets_at`, `block_start_at`,
2763
+ `seven_day_pct_at_block_start`) whenever it is present. Live ingest's
2764
+ upsert never updates those columns: the writer that materializes the
2765
+ row owns them. Two things separate that writer from the earliest
2766
+ surviving snapshot. A five-hour credit's stale-replica removal can
2767
+ delete its snapshot (the band starts at floor10 of the arming tick, so
2768
+ it reaches the genuine pre-credit reading); the snapshot still observed
2769
+ the block, because the removal retires its weekly VALUE, not the
2770
+ observation (J4). And a reading captured earlier can be appended, and
2771
+ so derived, later (fix round J5, 825-832-CD4-V01): its row survives
2772
+ with the earlier capture, but it updated a block another reading had
2773
+ already opened. Neither survivor-derived pick reproduces live. A
2774
+ window without an entry (a payload without an explicit account) falls
2775
+ back to the earliest surviving snapshot.
2776
+ * The projection stamps the computation-time pricing provenance of the
2777
+ totals it computes, through the live upsert's own helper
2778
+ (`_block_pricing_provenance_json`), so a successor's
2779
+ `five_hour_block_close` freezes it exactly as after live ingest. The
2780
+ ordinary (legacy, pre-journal) backfill keeps NULL: its blocks predate
2781
+ provenance, which `_lib_cost_provenance.historical_close_marker` reads
2782
+ as unproven.
2743
2783
  """
2744
2784
  _c = _cctally()
2745
2785
  inserted = 0
2786
+ journaled_first = (journaled_first or {}) if only_missing else {}
2787
+
2788
+ def _first_row(key, acct):
2789
+ """The window's start row: its first writer (J4/J5), else the
2790
+ earliest surviving snapshot."""
2791
+ journaled = journaled_first.get((acct, key))
2792
+ if journaled is not None:
2793
+ return journaled
2794
+ return conn.execute(
2795
+ """
2796
+ SELECT five_hour_resets_at, captured_at_utc, weekly_percent
2797
+ FROM weekly_usage_snapshots
2798
+ WHERE five_hour_window_key = ? AND account_key = ?
2799
+ AND five_hour_percent IS NOT NULL
2800
+ ORDER BY captured_at_utc ASC, id ASC LIMIT 1
2801
+ """,
2802
+ (key, acct),
2803
+ ).fetchone()
2804
+
2746
2805
  try:
2747
2806
  # Iterate distinct windows that have BOTH a canonical key AND a
2748
2807
  # 5h percent. The percent guard is critical: MAX(five_hour_percent)
@@ -2812,18 +2871,9 @@ def _backfill_five_hour_blocks(
2812
2871
  for key, acct in keys:
2813
2872
  if (acct, key) in seen_keys:
2814
2873
  continue
2815
- # Same MIN-captured pick the insert loop makes below, so the
2874
+ # Same start-row pick the insert loop makes below, so the
2816
2875
  # ownership interval and the inserted row cannot disagree.
2817
- anchor_row = conn.execute(
2818
- """
2819
- SELECT five_hour_resets_at
2820
- FROM weekly_usage_snapshots
2821
- WHERE five_hour_window_key = ? AND account_key = ?
2822
- AND five_hour_percent IS NOT NULL
2823
- ORDER BY captured_at_utc ASC, id ASC LIMIT 1
2824
- """,
2825
- (key, acct),
2826
- ).fetchone()
2876
+ anchor_row = _first_row(key, acct)
2827
2877
  if anchor_row is None:
2828
2878
  continue
2829
2879
  try:
@@ -2849,22 +2899,14 @@ def _backfill_five_hour_blocks(
2849
2899
  conn.execute("BEGIN IMMEDIATE")
2850
2900
  try:
2851
2901
  for key, acct in keys:
2852
- # MIN-captured row defines the immutable block boundary
2853
- # values (deterministic — picking "any in-window row"
2854
- # would be nondeterministic under seconds-level Anthropic
2855
- # ISO jitter that the canonical key collapses). Scoped to
2902
+ # The start row (the first writer, else the MIN-captured
2903
+ # survivor) defines the immutable block boundary values
2904
+ # (deterministic — picking "any in-window row" would be
2905
+ # nondeterministic under seconds-level Anthropic ISO jitter
2906
+ # that the canonical key collapses). Scoped to
2856
2907
  # (window_key, account_key) so a shared window's per-account
2857
2908
  # blocks each read their own account's boundary rows (#341).
2858
- min_row = conn.execute(
2859
- """
2860
- SELECT five_hour_resets_at, captured_at_utc, weekly_percent
2861
- FROM weekly_usage_snapshots
2862
- WHERE five_hour_window_key = ? AND account_key = ?
2863
- AND five_hour_percent IS NOT NULL
2864
- ORDER BY captured_at_utc ASC, id ASC LIMIT 1
2865
- """,
2866
- (key, acct),
2867
- ).fetchone()
2909
+ min_row = _first_row(key, acct)
2868
2910
  max_row = conn.execute(
2869
2911
  """
2870
2912
  SELECT captured_at_utc, weekly_percent, five_hour_percent
@@ -2941,9 +2983,15 @@ def _backfill_five_hour_blocks(
2941
2983
  # JSONL if cache.db is unreadable.
2942
2984
  # skip_sync=True: backfill runs inside open_db(); a sync_cache
2943
2985
  # cascade here would reopen cache.db recursively.
2986
+ pricing_evidence = {} if only_missing else None
2944
2987
  totals = _c._compute_block_totals(
2945
2988
  block_start_dt, last_obs_dt, skip_sync=True,
2946
2989
  owner_key=key, windows=ownership.get(acct, []),
2990
+ pricing_evidence=pricing_evidence,
2991
+ )
2992
+ pricing_provenance_json = (
2993
+ _c._block_pricing_provenance_json(pricing_evidence)
2994
+ if pricing_evidence is not None else None
2947
2995
  )
2948
2996
 
2949
2997
  cur = conn.execute(
@@ -2963,12 +3011,14 @@ def _backfill_five_hour_blocks(
2963
3011
  total_cache_create_tokens,
2964
3012
  total_cache_read_tokens,
2965
3013
  total_cost_usd,
3014
+ pricing_provenance_json,
2966
3015
  is_closed,
2967
3016
  created_at_utc,
2968
3017
  last_updated_at_utc,
2969
3018
  account_key
2970
3019
  )
2971
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
3020
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
3021
+ ?)
2972
3022
  """,
2973
3023
  (
2974
3024
  key,
@@ -2985,6 +3035,7 @@ def _backfill_five_hour_blocks(
2985
3035
  totals["cache_create_tokens"],
2986
3036
  totals["cache_read_tokens"],
2987
3037
  totals["cost_usd"],
3038
+ pricing_provenance_json,
2988
3039
  is_closed,
2989
3040
  projection_created_at,
2990
3041
  projection_updated_at,
@@ -97,6 +97,7 @@ from _lib_forecast import (
97
97
  # exercises). doctor's _iso_z deliberately keeps its own divergent copy.
98
98
  _ensure_sibling_loaded("_lib_json_envelope")
99
99
  from _lib_json_envelope import _iso_z
100
+ from _lib_five_hour import latest_five_hour_reading
100
101
 
101
102
  # #620 S1 D11: the one affordance shape every warning state renders.
102
103
  _ensure_sibling_loaded("_lib_alert_scope")
@@ -153,10 +154,30 @@ def _shape_week_samples(rows, *, include_held: bool,
153
154
  return out
154
155
 
155
156
 
157
+ def _week_rows_by_capture(cursor, *, by_id: bool) -> list:
158
+ """One week's sample rows in capture order: ``captured_at_utc`` ascending,
159
+ then ``id`` when ``by_id`` (the row's trailing column, which this strips).
160
+
161
+ #901 Amendment 19 T1 (spec I1): the reader returns every row of the week
162
+ anyway, so ordering them here costs no structure beyond its own result,
163
+ where ``ORDER BY captured_at_utc`` sorted the whole week through a temp
164
+ b-tree — and one subscription week is not a bound. ``list.sort`` is stable
165
+ over the same scan SQLite's in-memory sorter consumed, so rows tied on one
166
+ capture instant keep the order they had.
167
+ """
168
+ rows = cursor.fetchall()
169
+ if by_id:
170
+ rows.sort(key=lambda row: (row[0], row[-1]))
171
+ return [row[:-1] for row in rows]
172
+ rows.sort(key=lambda row: row[0])
173
+ return rows
174
+
175
+
156
176
  def _fetch_current_week_snapshots(conn: sqlite3.Connection, now_utc: dt.datetime,
157
177
  *, account_key: "str | None" = None,
158
178
  include_held: bool = False,
159
- include_account: bool = False):
179
+ include_account: bool = False,
180
+ include_scope: bool = False):
160
181
  """Return (week_start_at, week_end_at, list[(captured_at, percent, five_hr)])
161
182
  for the subscription week containing `now_utc`, or None if no snapshot
162
183
  exists for the current week.
@@ -206,6 +227,14 @@ def _fetch_current_week_snapshots(conn: sqlite3.Connection, now_utc: dt.datetime
206
227
  ``include_account`` appends the physical account key after the held flag
207
228
  for the multi-account Current Week card only. The default wire-independent
208
229
  sample shape and merged forecast/cost reads remain unchanged.
230
+
231
+ ``include_scope`` (#825/#832 spec 4.7 revision 7) appends a fourth element,
232
+ ``(week_start_date, week_start_at_texts)``: the week scope the samples were
233
+ read under, in the form ``latest_five_hour_reading`` takes
234
+ (``week_start_at_texts`` is ``None`` on the date-only fallback). A caller
235
+ reading the five-hour axis selects its reading under exactly this scope
236
+ rather than from the newest sample, which a retired replica or a
237
+ weekly-only reading leaves without a five-hour value.
209
238
  """
210
239
  # #341: optional account scoping — same predicate on every leg so the whole
211
240
  # window resolution stays consistent (a real key or the `unattributed`
@@ -264,6 +293,8 @@ def _fetch_current_week_snapshots(conn: sqlite3.Connection, now_utc: dt.datetime
264
293
  we_date = dt.date.fromisoformat(drow[1])
265
294
  week_start_at = dt.datetime.combine(ws_date, dt.time(0, 0), local_tz).astimezone(dt.timezone.utc)
266
295
  week_end_at = dt.datetime.combine(we_date + dt.timedelta(days=1), dt.time(0, 0), local_tz).astimezone(dt.timezone.utc)
296
+ # One date: `idx_usage_week_time` (week_start_date, captured_at_utc
297
+ # DESC, id DESC) serves this order outright, with no sorter.
267
298
  rows = conn.execute(
268
299
  "SELECT captured_at_utc, weekly_percent, five_hour_percent, "
269
300
  " weekly_observation_held" + _owner_column + " "
@@ -275,6 +306,8 @@ def _fetch_current_week_snapshots(conn: sqlite3.Connection, now_utc: dt.datetime
275
306
  samples = _shape_week_samples(rows, include_held=include_held,
276
307
  include_account=include_account)
277
308
  samples = [s for s in samples if s[0] <= now_utc]
309
+ if include_scope:
310
+ return week_start_at, week_end_at, samples, (drow[0], None)
278
311
  return week_start_at, week_end_at, samples
279
312
  row = chosen
280
313
  week_start_at = parse_iso_datetime(row[0], "week_start_at")
@@ -293,19 +326,22 @@ def _fetch_current_week_snapshots(conn: sqlite3.Connection, now_utc: dt.datetime
293
326
  matching_texts.append(r[0])
294
327
  chosen_date = chosen[2]
295
328
  placeholders = ",".join("?" * len(matching_texts))
296
- rows = conn.execute(
329
+ rows = _week_rows_by_capture(conn.execute(
297
330
  f"SELECT captured_at_utc, weekly_percent, five_hour_percent, "
298
- f" weekly_observation_held{_owner_column} "
331
+ f" weekly_observation_held{_owner_column}"
332
+ f"{', id' if include_account else ''} "
299
333
  f"FROM weekly_usage_snapshots "
300
334
  f"WHERE (week_start_at IN ({placeholders}) "
301
335
  f" OR (week_start_at IS NULL AND week_start_date = ?))"
302
- f"{_acct_pred}{_held_pred} "
303
- f"ORDER BY captured_at_utc{_sample_order}",
336
+ f"{_acct_pred}{_held_pred}",
304
337
  tuple(matching_texts) + (chosen_date,) + _acct_p,
305
- ).fetchall()
338
+ ), by_id=include_account)
306
339
  samples = _shape_week_samples(rows, include_held=include_held,
307
340
  include_account=include_account)
308
341
  samples = [s for s in samples if s[0] <= now_utc]
342
+ if include_scope:
343
+ return (week_start_at, week_end_at, samples,
344
+ (chosen_date, tuple(matching_texts)))
309
345
  return week_start_at, week_end_at, samples
310
346
 
311
347
 
@@ -353,6 +389,42 @@ def _apply_midweek_reset_override(
353
389
  return week_start_at, samples
354
390
 
355
391
 
392
+ def _current_week_five_hour_percent(conn, now_utc, week_scope, *,
393
+ account_key, week_start_at,
394
+ week_start_before_override):
395
+ """The percentage of ``_current_week_five_hour_reading``, or ``None``."""
396
+ reading = _current_week_five_hour_reading(
397
+ conn, now_utc, week_scope, account_key=account_key,
398
+ week_start_at=week_start_at,
399
+ week_start_before_override=week_start_before_override)
400
+ return None if reading is None else reading.percent
401
+
402
+
403
+ def _current_week_five_hour_reading(conn, now_utc, week_scope, *,
404
+ account_key, week_start_at,
405
+ week_start_before_override):
406
+ """The current week's five-hour reading: the newest five-hour READING
407
+ under the current-week read's own scope (#825/#832 spec 4.7 revision 7).
408
+ Its percent and reset come from one row, so a caller that shows both
409
+ shows one reading.
410
+
411
+ ``week_scope`` is ``_fetch_current_week_snapshots(include_scope=True)``'s
412
+ fourth element. When ``_apply_midweek_reset_override`` moved the week's
413
+ start, its new start bounds the captures exactly as it bounded the samples.
414
+ ``account_key`` ``None`` keeps the merged read's account-blind contract.
415
+ """
416
+ week_start_date, week_start_at_texts = week_scope
417
+ floor = (week_start_at if week_start_at != week_start_before_override
418
+ else None)
419
+ reading = latest_five_hour_reading(
420
+ conn, account_key=account_key,
421
+ as_of_utc=_iso_z(now_utc),
422
+ captured_at_or_after_utc=None if floor is None else _iso_z(floor),
423
+ week_start_date=week_start_date,
424
+ week_start_at_in=week_start_at_texts)
425
+ return reading
426
+
427
+
356
428
  def _resolve_current_budget_window(conn, now_utc, *, account_key=None):
357
429
  """Return ``(effective_week_start_dt, week_end_dt)`` for the subscription
358
430
  week containing ``now_utc``, honoring a mid-week reset re-anchor; or
@@ -786,8 +858,13 @@ def _dpp_candidate_comparability(regime, week_start_at, week_end_at, *,
786
858
 
787
859
  - `"comparable"` — in regime, and its own population passes the support
788
860
  test.
789
- - `"incomparable"` — in regime, population read, support test failed.
790
- This is drift, and only this.
861
+ - `"incomparable"` — in regime, population read, and `apply_regime`
862
+ rejected it. That is drift (the support test failed), or a week outside
863
+ the regime's coefficient era (`unvalidated-coefficient-era`, #978 spec
864
+ 4.6), whose units were weighted with another era's coefficients and so
865
+ cannot be compared with this regime's rate either, or a week with no
866
+ local population to weigh (`no-local-history`). All three mean the
867
+ same thing to the selector: the candidate is not comparable.
791
868
  - `"excluded"` — on the far side of a rate boundary.
792
869
  - `"unreadable"` — the population could not be read at all. Folding this
793
870
  into `"excluded"` made a locked `cache.db` look like twelve weeks that
@@ -816,7 +893,8 @@ def _dpp_candidate_comparability(regime, week_start_at, week_end_at, *,
816
893
  cache_conn_factory=cache_conn_factory)
817
894
  except Exception: # noqa: BLE001
818
895
  return "unreadable"
819
- applied = qcg.apply_regime(regime, entries)
896
+ applied = qcg.apply_regime(regime, entries,
897
+ interval=(week_start_at, week_end_at))
820
898
  if isinstance(applied, qcg.ApplyRejection):
821
899
  return "incomparable"
822
900
  return "comparable"
@@ -1268,7 +1346,12 @@ def _calibrated_week_detail(
1268
1346
  week_start_at, horizon, account_key=account_key)
1269
1347
  except sqlite3.Error:
1270
1348
  return CalibratedWeek(code="unavailable")
1271
- applied = qcg.apply_regime(read.regime, entries)
1349
+ # The interval is the whole week the projection extrapolates over, not
1350
+ # the entries' span: a week straddling the regime's coefficient era is
1351
+ # refused as `unvalidated-coefficient-era` even when every retained entry
1352
+ # so far is in-era (#978 spec 4.6).
1353
+ applied = qcg.apply_regime(read.regime, entries,
1354
+ interval=(week_start_at, week_end_at))
1272
1355
  if isinstance(applied, qcg.ApplyRejection):
1273
1356
  return CalibratedWeek(code=applied.value)
1274
1357
 
@@ -1371,12 +1454,14 @@ def _load_forecast_inputs(
1371
1454
  c = _cctally()
1372
1455
  # #769 S11 (#824): ONE read, held-inclusive, split per axis below.
1373
1456
  fetched = _fetch_current_week_snapshots(
1374
- conn, now_utc, account_key=account_key, include_held=True)
1457
+ conn, now_utc, account_key=account_key, include_held=True,
1458
+ include_scope=True)
1375
1459
  if fetched is None:
1376
1460
  return None
1377
- week_start_at, week_end_at, observations = fetched
1461
+ week_start_at, week_end_at, observations, week_scope = fetched
1378
1462
  if not observations:
1379
1463
  return None
1464
+ week_start_before_override = week_start_at
1380
1465
 
1381
1466
  # Mid-week reset override: shift week_start_at to the effective
1382
1467
  # reset moment and drop pre-reset samples so elapsed/remaining math
@@ -1400,8 +1485,16 @@ def _load_forecast_inputs(
1400
1485
  p_now = latest[1]
1401
1486
  # The five-hour axis, which is why the read is held-inclusive: a held row
1402
1487
  # exists precisely because its five-hour reading GREW, so the newest
1403
- # observation of either kind is the current five-hour percentage.
1404
- five_hr = observations[-1][2]
1488
+ # READING of either kind is the current five-hour percentage. It is
1489
+ # selected through the one shared selection (#825/#832 spec 4.7 revision
1490
+ # 7) under this read's own scope: the account, the week, `now_utc`, and
1491
+ # the mid-week reset's lower bound when the override applied. The newest
1492
+ # observation is not the answer, because a retired replica or a
1493
+ # weekly-only reading carries no five-hour value.
1494
+ five_hr = _current_week_five_hour_percent(
1495
+ conn, now_utc, week_scope, account_key=account_key,
1496
+ week_start_at=week_start_at,
1497
+ week_start_before_override=week_start_before_override)
1405
1498
 
1406
1499
  elapsed_hours = (now_utc - week_start_at).total_seconds() / 3600.0
1407
1500
  total_hours = (week_end_at - week_start_at).total_seconds() / 3600.0
@@ -2518,7 +2611,7 @@ def cmd_report(args: argparse.Namespace) -> int:
2518
2611
  m_rows: list[list[str]] = []
2519
2612
  for idx, m in enumerate(milestone_rows, start=1):
2520
2613
  pct = f"{int(m['percent_threshold'])}%"
2521
- cum = f"${float(m['cumulative_cost_usd']):.6f}"
2614
+ cum = c._fmt_usd_accounting(float(m['cumulative_cost_usd']), 6)
2522
2615
  marg = c.observation_gap_marginal_cell(
2523
2616
  m["marginal_cost_usd"],
2524
2617
  withheld=(idx - 1) in gap_disclosure.withheld_indexes,