cctally 1.108.0 → 1.110.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 (85) hide show
  1. package/CHANGELOG.md +147 -0
  2. package/README.md +6 -6
  3. package/bin/_cctally_account.py +4 -0
  4. package/bin/_cctally_alerts.py +6 -2
  5. package/bin/_cctally_cache.py +1652 -166
  6. package/bin/_cctally_config.py +3 -2
  7. package/bin/_cctally_core.py +594 -33
  8. package/bin/_cctally_dashboard.py +1585 -201
  9. package/bin/_cctally_dashboard_conversation.py +494 -48
  10. package/bin/_cctally_dashboard_envelope.py +167 -11
  11. package/bin/_cctally_dashboard_share.py +154 -36
  12. package/bin/_cctally_dashboard_sources.py +2615 -422
  13. package/bin/_cctally_db.py +678 -5
  14. package/bin/_cctally_diagnosis_sources.py +238 -26
  15. package/bin/_cctally_doctor.py +99 -18
  16. package/bin/_cctally_five_hour.py +1295 -40
  17. package/bin/_cctally_forecast.py +124 -28
  18. package/bin/_cctally_journal.py +408 -38
  19. package/bin/_cctally_milestone_history.py +150 -14
  20. package/bin/_cctally_parser.py +5 -0
  21. package/bin/_cctally_percent_breakdown.py +6 -1
  22. package/bin/_cctally_project.py +48 -76
  23. package/bin/_cctally_quota.py +39 -7
  24. package/bin/_cctally_quota_model.py +17 -2
  25. package/bin/_cctally_record.py +1647 -443
  26. package/bin/_cctally_rederive.py +899 -30
  27. package/bin/_cctally_refresh.py +2 -1
  28. package/bin/_cctally_share.py +24 -5
  29. package/bin/_cctally_source_analytics.py +1035 -78
  30. package/bin/_cctally_statusline.py +83 -19
  31. package/bin/_cctally_store.py +61 -4
  32. package/bin/_cctally_transcript.py +27 -2
  33. package/bin/_cctally_tui.py +264 -76
  34. package/bin/_cctally_weekrefs.py +28 -5
  35. package/bin/_lib_blocks.py +127 -20
  36. package/bin/_lib_cache_report.py +20 -5
  37. package/bin/_lib_codex_conversation_query.py +297 -64
  38. package/bin/_lib_codex_hooks.py +102 -0
  39. package/bin/_lib_codex_metadata.py +207 -0
  40. package/bin/_lib_conversation.py +119 -0
  41. package/bin/_lib_conversation_dispatch.py +31 -1
  42. package/bin/_lib_conversation_query.py +315 -51
  43. package/bin/_lib_conversation_retention.py +413 -25
  44. package/bin/_lib_dashboard_sources.py +267 -7
  45. package/bin/_lib_diagnosis.py +10 -0
  46. package/bin/_lib_diff_kernel.py +12 -1
  47. package/bin/_lib_doctor.py +230 -8
  48. package/bin/_lib_ingest_frontier.py +1366 -49
  49. package/bin/_lib_journal.py +19 -2
  50. package/bin/_lib_merge_gate.py +307 -0
  51. package/bin/_lib_pricing.py +588 -29
  52. package/bin/_lib_record.py +130 -12
  53. package/bin/_lib_rederive.py +903 -2
  54. package/bin/_lib_render.py +9 -3
  55. package/bin/_lib_retained_size.py +27 -2
  56. package/bin/_lib_segment_summary.py +66 -8
  57. package/bin/_lib_share_templates.py +28 -18
  58. package/bin/_lib_snapshot_cache.py +115 -1
  59. package/bin/_lib_source_retry.py +320 -0
  60. package/bin/_lib_statusline_candidates.py +400 -36
  61. package/bin/_lib_tick_stats.py +119 -0
  62. package/bin/_lib_view_models.py +256 -76
  63. package/bin/cctally +69 -0
  64. package/dashboard/static/assets/ConversationsView-BB9vUiL6.js +72 -0
  65. package/dashboard/static/assets/{DoctorModal-CWn3U4Wl.js → DoctorModal-DFY2iPDf.js} +1 -1
  66. package/dashboard/static/assets/ModalRoot-DvRKHbQE.js +1 -0
  67. package/dashboard/static/assets/ProjectsDrillPanel-DAv39uSo.js +1 -0
  68. package/dashboard/static/assets/SourceDetailModal-DSAWo2Fp.js +1 -0
  69. package/dashboard/static/assets/{UpdateModal-D3m8GG6V.js → UpdateModal-R_9dOVnh.js} +3 -3
  70. package/dashboard/static/assets/dashboardStream.shared-worker-DbG6Ef0T.js +1 -0
  71. package/dashboard/static/assets/index-COhxe3vU.css +1 -0
  72. package/dashboard/static/assets/index-DdU62sov.js +13 -0
  73. package/dashboard/static/assets/outlineNavigation-BOCLcRa6.js +9 -0
  74. package/dashboard/static/assets/useKeymap-CW7Up0Ay.js +1 -0
  75. package/dashboard/static/dashboard.html +3 -3
  76. package/package.json +4 -1
  77. package/dashboard/static/assets/ConversationsView-BOSaBRtu.js +0 -72
  78. package/dashboard/static/assets/ModalRoot-SR070V6c.js +0 -1
  79. package/dashboard/static/assets/ProjectsDrillPanel-QLI9i5mZ.js +0 -1
  80. package/dashboard/static/assets/SourceDetailModal-pv0wlFxl.js +0 -1
  81. package/dashboard/static/assets/dashboardStream.shared-worker-1XTMV3nr.js +0 -1
  82. package/dashboard/static/assets/index-BHu4mxd8.css +0 -1
  83. package/dashboard/static/assets/index-CIWsbux3.js +0 -13
  84. package/dashboard/static/assets/outlineNavigation-CJvKqmLV.js +0 -9
  85. package/dashboard/static/assets/useKeymap-ffqsS5G0.js +0 -1
@@ -13,6 +13,7 @@ No filesystem, SQLite, provider, alert, or journal operation belongs here.
13
13
  from __future__ import annotations
14
14
 
15
15
  import dataclasses
16
+ import datetime as dt
16
17
  import hashlib
17
18
  import json
18
19
  from collections.abc import Iterable, Mapping
@@ -45,6 +46,19 @@ class RederiveConflict(RederiveError):
45
46
  """The desired event set is internally inconsistent."""
46
47
 
47
48
 
49
+ class RederivePlanGuardConflict(RederiveConflict):
50
+ """A fully assembled plan is unsafe to preview or apply."""
51
+
52
+ def __init__(self, plan: "RederivePlan", plan_guard: dict):
53
+ super().__init__(
54
+ "week-reset add burst: "
55
+ f"{len(plan_guard['violations'])} group(s) exceed "
56
+ f"the limit of {plan_guard['limit']}"
57
+ )
58
+ self.plan = plan
59
+ self.plan_guard = plan_guard
60
+
61
+
48
62
  @dataclasses.dataclass(frozen=True)
49
63
  class KindClassification:
50
64
  mode: str
@@ -98,6 +112,8 @@ _EVT_CLASSIFICATIONS = {
98
112
  }
99
113
 
100
114
  _OP_CLASSIFICATIONS = {
115
+ "claude_weekly_observation_decision": KindClassification(
116
+ "retained_input", "reviewed weekly-axis decisions are replayed into scratch"),
101
117
  "weekly_credit_floor": KindClassification(
102
118
  "retained_input", "operator credit truth is replayed into scratch"),
103
119
  "account_observe": KindClassification(
@@ -303,6 +319,61 @@ def _preserve_non_derivable_state(
303
319
  """
304
320
  current_payload = current_record.get("payload") or {}
305
321
  desired_payload = desired_record.get("payload") or {}
322
+ if (
323
+ current_payload.get("kind") == "weekly_cost_snapshot"
324
+ and desired_payload.get("kind") == "weekly_cost_snapshot"
325
+ and current_record.get("at") == desired_record.get("at")
326
+ ):
327
+ # A live ingest writes range bounds in the configured display zone,
328
+ # while scratch replay can recover the same instants with the source
329
+ # session's offset. Offset spelling is not a cost correction. Keep the
330
+ # selected payload byte-for-byte when both bounds name the same
331
+ # instants and every other priced fact agrees.
332
+ def cost_structure(payload):
333
+ facts = dict(payload)
334
+ for field in ("range_start_iso", "range_end_iso"):
335
+ instant = _iso_instant(facts.get(field))
336
+ if instant is not None:
337
+ facts[field] = instant.isoformat()
338
+ return facts
339
+
340
+ if cost_structure(current_payload) == cost_structure(desired_payload):
341
+ return current_record
342
+ if (
343
+ current_payload.get("kind") == "five_hour_block_close"
344
+ and desired_payload.get("kind") == "five_hour_block_close"
345
+ and current_record.get("at") == desired_record.get("at")
346
+ and current_payload.get("is_closed") == 1
347
+ and desired_payload.get("is_closed") == 1
348
+ ):
349
+ # The close event is the monetary freeze boundary. A newer ownership
350
+ # rule or a later cache prefix may reprice its scratch projection, but
351
+ # cannot revise the already-journaled parent/child totals. Compare the
352
+ # closure's non-monetary evidence first so a changed boundary or final
353
+ # reading still reaches the normal correction path.
354
+ def structure(payload):
355
+ facts = {
356
+ key: value for key, value in payload.items()
357
+ if not key.startswith("total_")
358
+ and key not in {"_models", "_projects"}
359
+ }
360
+ # Live close used the display-zone offset; scratch replay writes
361
+ # UTC. Both name one frozen boundary and must compare as an instant.
362
+ start = facts.get("block_start_at")
363
+ if isinstance(start, str):
364
+ try:
365
+ parsed = dt.datetime.fromisoformat(
366
+ start.replace("Z", "+00:00"))
367
+ except ValueError:
368
+ pass
369
+ else:
370
+ if parsed.tzinfo is not None:
371
+ facts["block_start_at"] = parsed.astimezone(
372
+ dt.timezone.utc).isoformat()
373
+ return facts
374
+
375
+ if structure(current_payload) == structure(desired_payload):
376
+ return current_record
306
377
  if (
307
378
  current_payload.get("kind") != "percent_milestone"
308
379
  or desired_payload.get("kind") != "percent_milestone"
@@ -316,6 +387,615 @@ def _preserve_non_derivable_state(
316
387
  return merged
317
388
 
318
389
 
390
+ _SNAPSHOT_IDENTITY_FIELDS = (
391
+ "account_key", "week_start_date", "week_start_at", "week_end_at",
392
+ "weekly_percent", "five_hour_percent", "five_hour_window_key",
393
+ "weekly_observation_held", "source",
394
+ )
395
+ _SNAPSHOT_DEPENDENT_KINDS = frozenset({
396
+ "percent_milestone", "five_hour_milestone",
397
+ })
398
+ _EMPTY_REFERENCE_SENTINELS = frozenset({None, 0, "0", ""})
399
+ _MILESTONE_BASE_DEPENDENCIES = {
400
+ "usage_snapshot_ref": "snapshot_accept",
401
+ "cost_snapshot_ref": "weekly_cost_snapshot",
402
+ }
403
+
404
+
405
+ def _milestone_dependencies(kind: str) -> dict[str, str]:
406
+ dependencies = dict(_MILESTONE_BASE_DEPENDENCIES)
407
+ dependencies["reset_event_ref"] = (
408
+ "week_reset" if kind == "percent_milestone" else "five_hour_credit"
409
+ )
410
+ return dependencies
411
+
412
+ _BLOCK_WEEKLY_AXIS_FIELDS = frozenset({
413
+ "seven_day_pct_at_block_start",
414
+ "seven_day_pct_at_block_end",
415
+ "crossed_seven_day_reset",
416
+ })
417
+
418
+
419
+ def _observation_identity(record: Mapping):
420
+ """Comparable reading, only when every discriminator is retained."""
421
+ payload = record.get("payload") or {}
422
+ if payload.get("kind") != "snapshot_accept":
423
+ return None
424
+ if any(field not in payload for field in _SNAPSHOT_IDENTITY_FIELDS):
425
+ return None
426
+ reading = {
427
+ key: value for key, value in payload.items()
428
+ if key not in {"captured_at_utc", "payload_json"}
429
+ }
430
+ if "payload_json" in payload:
431
+ try:
432
+ raw = json.loads(payload["payload_json"])
433
+ except (TypeError, ValueError):
434
+ raw = payload["payload_json"]
435
+ if isinstance(raw, dict):
436
+ raw = dict(raw)
437
+ raw.pop("capturedAt", None)
438
+ raw.pop("captured_at", None)
439
+ reading["payload_json"] = raw
440
+ return _canonical_bytes(reading)
441
+
442
+
443
+ def _iso_instant(value):
444
+ if not isinstance(value, str):
445
+ return None
446
+ try:
447
+ parsed = dt.datetime.fromisoformat(value.replace("Z", "+00:00"))
448
+ except ValueError:
449
+ return None
450
+ return parsed.astimezone(dt.timezone.utc) if parsed.tzinfo else None
451
+
452
+
453
+ def _raw_supports_snapshot(raw: Mapping, event: Mapping, *, held: bool) -> bool:
454
+ """Bind a journaled acceptance to its exact retained origin observation."""
455
+ raw_payload = raw.get("payload") or {}
456
+ event_payload = event.get("payload") or {}
457
+ if (
458
+ raw.get("t") != "obs" or raw.get("provider") != "claude"
459
+ or raw.get("src") != "record-usage"
460
+ or event.get("id") != f"sa:{raw.get('id')}"
461
+ or event_payload.get("kind") != "snapshot_accept"
462
+ or raw.get("account") != event_payload.get("account_key")
463
+ or raw.get("at") != event.get("at")
464
+ or raw_payload.get("captured_at") != event_payload.get("captured_at_utc")
465
+ or raw_payload.get("source") != event_payload.get("source")
466
+ or raw_payload.get("five_hour_percent")
467
+ != event_payload.get("five_hour_percent")
468
+ or raw_payload.get("five_hour_resets_at")
469
+ != event_payload.get("five_hour_resets_at")
470
+ ):
471
+ return False
472
+ try:
473
+ encoded = json.loads(event_payload["payload_json"])
474
+ except (KeyError, TypeError, ValueError):
475
+ return False
476
+ if not isinstance(encoded, dict) or (
477
+ encoded.get("source") != raw_payload.get("source")
478
+ or encoded.get("capturedAt") != raw_payload.get("captured_at")
479
+ or encoded.get("fiveHourPercent")
480
+ != raw_payload.get("five_hour_percent")
481
+ ):
482
+ return False
483
+ if held:
484
+ return (
485
+ event_payload.get("weekly_observation_held") == 1
486
+ and encoded.get("weeklyObservationHeld") is True
487
+ and encoded.get("rawWeeklyPercent")
488
+ == raw_payload.get("weekly_percent")
489
+ and encoded.get("weeklyPercent")
490
+ == event_payload.get("weekly_percent")
491
+ )
492
+ return (
493
+ event_payload.get("weekly_observation_held") in (None, 0)
494
+ and encoded.get("weeklyObservationHeld") is not True
495
+ and encoded.get("weeklyPercent") == raw_payload.get("weekly_percent")
496
+ == event_payload.get("weekly_percent")
497
+ )
498
+
499
+
500
+ def _same_acceptance_transition(
501
+ old: Mapping, new: Mapping, old_raw: Mapping, new_raw: Mapping,
502
+ between: Iterable[Mapping], *,
503
+ allow_intervening_same_account_observations: bool = False,
504
+ ) -> bool:
505
+ """Recognize a replay-only candidate ahead of one retained acceptance.
506
+
507
+ The old non-held event remains the durable decision. The held candidate
508
+ carries a lower raw weekly reading into that same effective percentage;
509
+ it is not an equivalent raw observation. This check uses the exact two
510
+ retained origins and their order, with no elapsed-time or source-blind
511
+ pairing. If any causal discriminator differs, the normal diff remains.
512
+ """
513
+ old_payload = old.get("payload") or {}
514
+ new_payload = new.get("payload") or {}
515
+ old_raw_payload = old_raw.get("payload") or {}
516
+ new_raw_payload = new_raw.get("payload") or {}
517
+ if not (
518
+ _raw_supports_snapshot(old_raw, old, held=False)
519
+ and old_raw.get("account") == new_raw.get("account")
520
+ and _iso_instant(new.get("at")) is not None
521
+ and _iso_instant(old.get("at")) is not None
522
+ and _iso_instant(new["at"]) < _iso_instant(old["at"])
523
+ ):
524
+ return False
525
+ axes = (
526
+ "account_key", "week_start_date", "week_end_date",
527
+ "week_start_at", "week_end_at", "weekly_percent",
528
+ "five_hour_percent", "five_hour_window_key", "page_url",
529
+ )
530
+ if any(old_payload.get(axis) != new_payload.get(axis) for axis in axes):
531
+ return False
532
+ old_reset = _iso_instant(old_payload.get("five_hour_resets_at"))
533
+ new_reset = _iso_instant(new_payload.get("five_hour_resets_at"))
534
+ if (old_reset is None) != (new_reset is None):
535
+ return False
536
+ if old_reset is not None and abs(
537
+ (old_reset - new_reset).total_seconds()
538
+ ) > 1:
539
+ return False
540
+ try:
541
+ weekly_reset_gap = abs(
542
+ float(old_raw_payload["resets_at"])
543
+ - float(new_raw_payload["resets_at"])
544
+ )
545
+ except (KeyError, TypeError, ValueError):
546
+ return False
547
+ if weekly_reset_gap > 1:
548
+ return False
549
+
550
+ same_raw_reading = (
551
+ _raw_supports_snapshot(new_raw, new, held=False)
552
+ and old_raw_payload.get("weekly_percent")
553
+ == new_raw_payload.get("weekly_percent")
554
+ and old_raw_payload.get("source") == new_raw_payload.get("source")
555
+ and _observation_identity(old) == _observation_identity(new)
556
+ )
557
+ held_predecessor = (
558
+ _raw_supports_snapshot(new_raw, new, held=True)
559
+ and new_raw_payload.get("source") == "statusline"
560
+ and isinstance(old_raw_payload.get("weekly_percent"), (int, float))
561
+ and isinstance(new_raw_payload.get("weekly_percent"), (int, float))
562
+ and abs(
563
+ float(old_raw_payload["weekly_percent"])
564
+ - float(new_raw_payload["weekly_percent"]) - 1.0
565
+ ) < 1e-9
566
+ and new_payload.get("weekly_percent")
567
+ == old_raw_payload["weekly_percent"]
568
+ )
569
+ if not (same_raw_reading or held_predecessor):
570
+ return False
571
+
572
+ # The two origins must be consecutive within this account's complete
573
+ # replay input. Any retained operator record can change a floor, account,
574
+ # or cost dependency, and any intervening Claude observation for this
575
+ # account can change reset/credit/clamp state even when its displayed axes
576
+ # differ. Other providers and other Claude accounts cannot affect this
577
+ # account's fold.
578
+ for record in between:
579
+ if record.get("t") == "op":
580
+ return False
581
+ if (
582
+ record.get("t") == "obs"
583
+ and record.get("provider") == "claude"
584
+ and record.get("account") == old_raw.get("account")
585
+ and not allow_intervening_same_account_observations
586
+ ):
587
+ return False
588
+ return True
589
+
590
+
591
+ def _contains_value(value, wanted: str) -> bool:
592
+ if value == wanted:
593
+ return True
594
+ if isinstance(value, Mapping):
595
+ return any(_contains_value(item, wanted) for item in value.values())
596
+ if isinstance(value, (list, tuple)):
597
+ return any(_contains_value(item, wanted) for item in value)
598
+ return False
599
+
600
+
601
+ def _contains_any_value(value, wanted: set[str]) -> bool:
602
+ if isinstance(value, str):
603
+ return value in wanted
604
+ if isinstance(value, Mapping):
605
+ return any(_contains_any_value(item, wanted) for item in value.values())
606
+ if isinstance(value, (list, tuple)):
607
+ return any(_contains_any_value(item, wanted) for item in value)
608
+ return False
609
+
610
+
611
+ def _reconcile_snapshot_identities(
612
+ current: Mapping[str, Mapping], desired: dict[str, Mapping], selection,
613
+ raw_observations: Iterable[Mapping], snapshot_identity_decisions=(),
614
+ ) -> None:
615
+ """Keep an existing accepted decision across a replay policy change.
616
+
617
+ A held candidate may copy an already-observed weekly percent from scratch
618
+ state and be mistaken for the later genuine crossing that the journal
619
+ actually accepted. The two raw weekly readings differ. Keep the selected
620
+ decision only when exact retained origins prove their causal order, axes,
621
+ and dependency graph; otherwise leave the ordinary correction diff intact.
622
+ """
623
+ if not isinstance(raw_observations, (list, tuple)):
624
+ raw_observations = tuple(raw_observations)
625
+ reviewed_pairs = tuple(snapshot_identity_decisions or ())
626
+ if not raw_observations and not reviewed_pairs:
627
+ return
628
+ reviewed_set = set()
629
+ identity_ids = set()
630
+ for pair in reviewed_pairs:
631
+ if (not isinstance(pair, (list, tuple)) or len(pair) != 2
632
+ or not all(isinstance(item, str) and item for item in pair)
633
+ or pair[0] == pair[1]
634
+ or pair[0] in identity_ids or pair[1] in identity_ids):
635
+ raise RederiveConflict(
636
+ "malformed or ambiguous snapshot identity decision"
637
+ )
638
+ identity_ids.update(pair)
639
+ reviewed_set.add(tuple(pair))
640
+ for raw_id in identity_ids:
641
+ if sum(record.get("id") == raw_id for record in raw_observations) != 1:
642
+ raise RederiveConflict(
643
+ "snapshot identity decision raw ids must occur exactly once: "
644
+ + raw_id
645
+ )
646
+ needed_raw_ids = {
647
+ event_id[3:] for event_id, record in current.items()
648
+ if event_id.startswith("sa:") and event_id not in desired
649
+ and (record.get("payload") or {}).get("kind") == "snapshot_accept"
650
+ } | {
651
+ event_id[3:] for event_id, record in desired.items()
652
+ if event_id.startswith("sa:") and event_id not in current
653
+ and event_id not in selection.by_id
654
+ and (record.get("payload") or {}).get("kind") == "snapshot_accept"
655
+ }
656
+ if not needed_raw_ids:
657
+ return
658
+ raw_positions = {}
659
+ repeated_raw_ids = set()
660
+ for index, record in enumerate(raw_observations):
661
+ raw_id = record.get("id")
662
+ if (
663
+ record.get("t") != "obs"
664
+ or record.get("provider") != "claude"
665
+ or raw_id not in needed_raw_ids
666
+ ):
667
+ continue
668
+ if raw_id in raw_positions:
669
+ repeated_raw_ids.add(raw_id)
670
+ else:
671
+ raw_positions[raw_id] = (index, record)
672
+ # A repeated physical raw id has no unique order position. Leave its
673
+ # accepted decision to the normal diff instead of choosing an occurrence.
674
+ for raw_id in repeated_raw_ids:
675
+ del raw_positions[raw_id]
676
+ unmatched_current = {
677
+ event_id: record for event_id, record in current.items()
678
+ if event_id not in desired
679
+ and event_id.startswith("sa:")
680
+ and event_id[3:] in raw_positions
681
+ and (record.get("payload") or {}).get("kind") == "snapshot_accept"
682
+ }
683
+ unmatched_desired = {
684
+ event_id: record for event_id, record in desired.items()
685
+ if event_id not in current
686
+ and event_id not in selection.by_id
687
+ and event_id.startswith("sa:")
688
+ and event_id[3:] in raw_positions
689
+ and (record.get("payload") or {}).get("kind") == "snapshot_accept"
690
+ }
691
+ candidates: dict[str, list[str]] = {}
692
+ for old_id, old in unmatched_current.items():
693
+ old_index, old_raw = raw_positions[old_id[3:]]
694
+ candidates[old_id] = []
695
+ for new_id, new in unmatched_desired.items():
696
+ new_index, new_raw = raw_positions[new_id[3:]]
697
+ if (
698
+ new_index < old_index
699
+ and _same_acceptance_transition(
700
+ old, new, old_raw, new_raw,
701
+ raw_observations[new_index + 1:old_index],
702
+ allow_intervening_same_account_observations=(
703
+ (old_id[3:], new_id[3:]) in reviewed_set
704
+ ),
705
+ )
706
+ ):
707
+ candidates[old_id].append(new_id)
708
+
709
+ consumed_reviewed = set()
710
+ for old_id, matches in sorted(candidates.items()):
711
+ if len(matches) != 1:
712
+ continue
713
+ new_id = matches[0]
714
+ if sum(new_id in group for group in candidates.values()) != 1:
715
+ continue
716
+ old = unmatched_current[old_id]
717
+ new = unmatched_desired[new_id]
718
+ # Implicit sync-week events are keyed by the originating raw
719
+ # observation too. A percent milestone can reference both the usage
720
+ # snapshot and the cost snapshot from that observation. Preserve the
721
+ # cost identity only when its complete priced content agrees; the two
722
+ # capture bounds may differ with the accepted origins.
723
+ old_cost_prefix = f"wcs:{old_id[3:]}:"
724
+ new_cost_prefix = f"wcs:{new_id[3:]}:"
725
+ cost_pairs = {}
726
+ for old_cost_id, old_cost in current.items():
727
+ if not old_cost_id.startswith(old_cost_prefix):
728
+ continue
729
+ new_cost_id = new_cost_prefix + old_cost_id[len(old_cost_prefix):]
730
+ new_cost = desired.get(new_cost_id)
731
+ if new_cost is None or new_cost_id in current:
732
+ continue
733
+ old_cost_payload = old_cost.get("payload") or {}
734
+ new_cost_payload = new_cost.get("payload") or {}
735
+ if (
736
+ old_cost_payload.get("kind") != "weekly_cost_snapshot"
737
+ or new_cost_payload.get("kind") != "weekly_cost_snapshot"
738
+ ):
739
+ continue
740
+ def priced_content(payload):
741
+ return {
742
+ key: value for key, value in payload.items()
743
+ if key not in {"captured_at_utc", "range_end_iso"}
744
+ }
745
+ if priced_content(old_cost_payload) == priced_content(new_cost_payload):
746
+ cost_pairs[new_cost_id] = (old_cost_id, old_cost)
747
+ dependent_updates = {}
748
+ safe = True
749
+ for event_id in set(current) | set(desired):
750
+ now = current.get(event_id)
751
+ proposed = desired.get(event_id)
752
+ now_payload = (now or {}).get("payload") or {}
753
+ proposed_payload = (proposed or {}).get("payload") or {}
754
+ refers_old = _contains_value(now_payload, old_id) or any(
755
+ _contains_value(now_payload, old_cost_id)
756
+ for old_cost_id, _ in cost_pairs.values()
757
+ )
758
+ refers_new = _contains_value(proposed_payload, new_id) or any(
759
+ _contains_value(proposed_payload, new_cost_id)
760
+ for new_cost_id in cost_pairs
761
+ )
762
+ if not (refers_old or refers_new):
763
+ continue
764
+ if (
765
+ now is None or proposed is None
766
+ or now_payload.get("kind") not in _SNAPSHOT_DEPENDENT_KINDS
767
+ or proposed_payload.get("kind") != now_payload.get("kind")
768
+ or now_payload.get("usage_snapshot_ref") != old_id
769
+ or proposed_payload.get("usage_snapshot_ref") != new_id
770
+ or any(
771
+ now_payload.get(field) != proposed_payload.get(field)
772
+ for field in (
773
+ "account_key", "week_start_date",
774
+ "five_hour_window_key", "reset_event_ref",
775
+ "percent_threshold",
776
+ )
777
+ )
778
+ or _contains_value(
779
+ {k: v for k, v in proposed_payload.items()
780
+ if k != "usage_snapshot_ref"}, new_id,
781
+ )
782
+ ):
783
+ safe = False
784
+ break
785
+ new_cost_ref = proposed_payload.get("cost_snapshot_ref")
786
+ if new_cost_ref in cost_pairs:
787
+ old_cost_ref = cost_pairs[new_cost_ref][0]
788
+ if now_payload.get("cost_snapshot_ref") != old_cost_ref:
789
+ safe = False
790
+ break
791
+ elif new_cost_ref != now_payload.get("cost_snapshot_ref"):
792
+ safe = False
793
+ break
794
+ # A crossing is a retained fact of the selected acceptance,
795
+ # including observed weekly axis, capture clock and money. The
796
+ # scratch candidate's ref/clock/cost belong to the rejected held
797
+ # origin. Keep the whole selected dependent event.
798
+ dependent_updates[event_id] = now
799
+ if not safe:
800
+ continue
801
+ del desired[new_id]
802
+ desired[old_id] = old
803
+ for new_cost_id, (old_cost_id, old_cost) in cost_pairs.items():
804
+ del desired[new_cost_id]
805
+ desired[old_cost_id] = old_cost
806
+ desired.update(dependent_updates)
807
+ pair = (old_id[3:], new_id[3:])
808
+ if pair in reviewed_set:
809
+ consumed_reviewed.add(pair)
810
+
811
+ unused = reviewed_set - consumed_reviewed
812
+ if unused:
813
+ accepted, replay = sorted(unused)[0]
814
+ raise RederiveConflict(
815
+ "snapshot identity decision failed automatic account, window, "
816
+ "axis, binding, dependency, or uniqueness checks: "
817
+ f"{accepted} -> {replay}"
818
+ )
819
+
820
+
821
+ def _preserve_reviewed_hold_dependencies(
822
+ current: Mapping[str, Mapping], desired: dict[str, Mapping],
823
+ reviewed_weekly_hold_ids=(),
824
+ ) -> None:
825
+ """Keep frozen five-hour facts reached from exact reviewed holds.
826
+
827
+ An exact weekly-axis hold must not erase the same observation's retained
828
+ five-hour crossing. Milestones are historical facts, so retain their whole
829
+ accepted payload and close the usage/cost reference graph with the exact
830
+ currently selected events. A closed block may update only its weekly axes;
831
+ its five-hour close boundary, reading, money, tokens and children stay the
832
+ selected frozen fact. Any unfamiliar reference shape refuses rather than
833
+ being guessed or rewritten.
834
+ """
835
+ snapshot_ids = {
836
+ f"sa:{raw_id}" for raw_id in reviewed_weekly_hold_ids
837
+ if isinstance(raw_id, str) and raw_id
838
+ }
839
+ if not snapshot_ids:
840
+ return
841
+ suppressed_snapshot_ids = set()
842
+ for record in (*current.values(), *desired.values()):
843
+ suppression = (record.get("payload") or {}).get("suppression")
844
+ if suppression is None:
845
+ continue
846
+ if not isinstance(suppression, list) or any(
847
+ not isinstance(item, str) for item in suppression
848
+ ):
849
+ raise RederiveConflict(
850
+ "reviewed weekly hold suppression has an unfamiliar shape: "
851
+ + str(record.get("id"))
852
+ )
853
+ suppressed_snapshot_ids.update(snapshot_ids & set(suppression))
854
+ if suppressed_snapshot_ids:
855
+ for event_id, record in (*current.items(), *desired.items()):
856
+ payload = record.get("payload") or {}
857
+ if (payload.get("kind") in _SNAPSHOT_DEPENDENT_KINDS
858
+ and payload.get("usage_snapshot_ref")
859
+ in suppressed_snapshot_ids):
860
+ raise RederiveConflict(
861
+ "reviewed weekly hold suppression would leave a dependent "
862
+ "milestone: " + event_id
863
+ )
864
+ # A retained reset or credit suppression is the historical fact that
865
+ # this snapshot was deleted. Make that destructive effect explicit in
866
+ # the target graph instead of resurrecting the held observation's
867
+ # snapshot while silently retaining the suppression beside it.
868
+ for snapshot_id in suppressed_snapshot_ids:
869
+ desired.pop(snapshot_id, None)
870
+ snapshot_ids -= suppressed_snapshot_ids
871
+ for event_id, record in current.items():
872
+ payload = record.get("payload") or {}
873
+ usage_ref = payload.get("usage_snapshot_ref")
874
+ if usage_ref not in snapshot_ids:
875
+ if _contains_any_value(payload, snapshot_ids):
876
+ raise RederiveConflict(
877
+ "reviewed weekly hold dependency has an unfamiliar shape: "
878
+ + event_id
879
+ )
880
+ continue
881
+ if payload.get("kind") not in _SNAPSHOT_DEPENDENT_KINDS:
882
+ raise RederiveConflict(
883
+ "reviewed weekly hold dependency has an unfamiliar kind: "
884
+ + event_id
885
+ )
886
+ snapshot = current.get(usage_ref)
887
+ if ((snapshot or {}).get("payload") or {}).get("kind") != "snapshot_accept":
888
+ raise RederiveConflict(
889
+ "reviewed weekly hold dependency lacks its accepted snapshot: "
890
+ + event_id
891
+ )
892
+ desired[event_id] = record
893
+ desired[usage_ref] = snapshot
894
+ for field, expected_kind in _milestone_dependencies(
895
+ payload.get("kind")
896
+ ).items():
897
+ if field == "usage_snapshot_ref":
898
+ continue
899
+ reference = payload.get(field)
900
+ if reference in _EMPTY_REFERENCE_SENTINELS:
901
+ continue
902
+ dependency = current.get(reference)
903
+ if ((dependency or {}).get("payload") or {}).get("kind") \
904
+ != expected_kind:
905
+ raise RederiveConflict(
906
+ f"reviewed weekly hold dependency lacks its {field}: "
907
+ f"{event_id}"
908
+ )
909
+ desired[reference] = dependency
910
+
911
+ # The reviewed replay can introduce a milestone for an accepted reset
912
+ # while the exact snapshot it cites is independently held. Close those
913
+ # newly introduced edges against the currently selected graph too; looking
914
+ # only at current milestones leaves the new event pointing at a snapshot
915
+ # the same plan tombstones.
916
+ for event_id, record in tuple(desired.items()):
917
+ payload = record.get("payload") or {}
918
+ usage_ref = payload.get("usage_snapshot_ref")
919
+ if usage_ref not in snapshot_ids:
920
+ if _contains_any_value(payload, snapshot_ids):
921
+ raise RederiveConflict(
922
+ "reviewed weekly hold dependency has an unfamiliar shape: "
923
+ + event_id
924
+ )
925
+ continue
926
+ if payload.get("kind") not in _SNAPSHOT_DEPENDENT_KINDS:
927
+ raise RederiveConflict(
928
+ "reviewed weekly hold dependency has an unfamiliar kind: "
929
+ + event_id
930
+ )
931
+ snapshot = current.get(usage_ref)
932
+ if ((snapshot or {}).get("payload") or {}).get("kind") \
933
+ != "snapshot_accept":
934
+ # A fresh replay can derive a crossing from the same observation
935
+ # whose weekly axis the review holds. With no selected snapshot to
936
+ # preserve, that crossing has no valid historical dependency and
937
+ # must leave the reviewed target with the held snapshot.
938
+ desired.pop(event_id, None)
939
+ continue
940
+ desired[usage_ref] = snapshot
941
+ for field, expected_kind in _milestone_dependencies(
942
+ payload.get("kind")
943
+ ).items():
944
+ if field == "usage_snapshot_ref":
945
+ continue
946
+ reference = payload.get(field)
947
+ if reference in _EMPTY_REFERENCE_SENTINELS:
948
+ continue
949
+ dependency = current.get(reference)
950
+ if ((dependency or {}).get("payload") or {}).get("kind") \
951
+ != expected_kind:
952
+ raise RederiveConflict(
953
+ f"reviewed weekly hold dependency lacks its {field}: "
954
+ f"{event_id}"
955
+ )
956
+ desired[reference] = dependency
957
+
958
+ held_windows = set()
959
+ for snapshot_id in snapshot_ids:
960
+ payload = (current.get(snapshot_id) or {}).get("payload") or {}
961
+ if payload.get("kind") != "snapshot_accept":
962
+ continue
963
+ account_key = payload.get("account_key")
964
+ window_key = payload.get("five_hour_window_key")
965
+ if account_key is not None and window_key is not None:
966
+ held_windows.add((account_key, window_key))
967
+ for event_id, record in current.items():
968
+ payload = record.get("payload") or {}
969
+ if (
970
+ payload.get("kind") != "five_hour_block_close"
971
+ or (payload.get("account_key"), payload.get("five_hour_window_key"))
972
+ not in held_windows
973
+ ):
974
+ continue
975
+ proposed = desired.get(event_id)
976
+ if proposed is None:
977
+ desired[event_id] = record
978
+ continue
979
+ proposed_payload = proposed.get("payload") or {}
980
+ if (
981
+ proposed_payload.get("kind") != "five_hour_block_close"
982
+ or proposed_payload.get("account_key") != payload.get("account_key")
983
+ or proposed_payload.get("five_hour_window_key")
984
+ != payload.get("five_hour_window_key")
985
+ ):
986
+ raise RederiveConflict(
987
+ "reviewed weekly hold block has an unfamiliar shape: "
988
+ + event_id
989
+ )
990
+ merged_payload = dict(payload)
991
+ for field in _BLOCK_WEEKLY_AXIS_FIELDS:
992
+ if field in proposed_payload:
993
+ merged_payload[field] = proposed_payload[field]
994
+ merged = dict(record)
995
+ merged["payload"] = merged_payload
996
+ desired[event_id] = merged
997
+
998
+
319
999
  @dataclasses.dataclass(frozen=True)
320
1000
  class PlanAction:
321
1001
  disposition: str
@@ -324,6 +1004,7 @@ class PlanAction:
324
1004
  at: str
325
1005
  payload: "dict | None"
326
1006
  payload_hash: str
1007
+ kind: str
327
1008
 
328
1009
  def to_dict(self) -> dict:
329
1010
  out = {
@@ -365,6 +1046,9 @@ class RederivePlan:
365
1046
  actions: tuple[PlanAction, ...]
366
1047
  retained_event_count: int
367
1048
  preserved_event_count: int = 0
1049
+ action_counts_by_event_kind: Mapping[str, Mapping[str, int]] = dataclasses.field(
1050
+ default_factory=dict
1051
+ )
368
1052
 
369
1053
  def _body(self) -> dict:
370
1054
  return {
@@ -418,7 +1102,11 @@ def build_claude_usage_plan(*, selection, desired_events: Iterable[Mapping],
418
1102
  cache_fingerprint: str,
419
1103
  config_fingerprint: str,
420
1104
  preserved_events: Iterable[Mapping],
421
- conflicted_event_ids=frozenset()) -> RederivePlan:
1105
+ conflicted_event_ids=frozenset(),
1106
+ raw_observations=(),
1107
+ snapshot_identity_decisions=(),
1108
+ reviewed_weekly_hold_ids=(),
1109
+ enforce_guard=True) -> RederivePlan:
422
1110
  """Diff current effective events against one scratch-derived desired set.
423
1111
 
424
1112
  ``preserved_events`` (#426) names the owned events the re-derivation cannot
@@ -448,6 +1136,13 @@ def build_claude_usage_plan(*, selection, desired_events: Iterable[Mapping],
448
1136
  and selected.record is not None
449
1137
  and _is_owned_event(selected.record)
450
1138
  }
1139
+ _reconcile_snapshot_identities(
1140
+ current, desired, selection, raw_observations,
1141
+ snapshot_identity_decisions,
1142
+ )
1143
+ _preserve_reviewed_hold_dependencies(
1144
+ current, desired, reviewed_weekly_hold_ids,
1145
+ )
451
1146
  retained_event_count = sum(
452
1147
  1 for selected in selection.by_id.values()
453
1148
  if selected.status == "active"
@@ -460,6 +1155,10 @@ def build_claude_usage_plan(*, selection, desired_events: Iterable[Mapping],
460
1155
  if isinstance(event_id, str) and event_id and event_id not in desired:
461
1156
  preserved[event_id] = record
462
1157
  counts = {"retain": 0, "supersede": 0, "tombstone": 0, "add": 0}
1158
+ counts_by_kind = {
1159
+ kind: dict.fromkeys(counts, 0)
1160
+ for kind in sorted(_EVT_CLASSIFICATIONS)
1161
+ }
463
1162
  actions: list[PlanAction] = []
464
1163
 
465
1164
  for event_id in sorted(set(current) | set(desired) | set(preserved)):
@@ -482,6 +1181,9 @@ def build_claude_usage_plan(*, selection, desired_events: Iterable[Mapping],
482
1181
  )
483
1182
  if not (revive or reaffirm):
484
1183
  counts["retain"] += 1
1184
+ counts_by_kind[(preserved_record.get("payload") or {})["kind"]][
1185
+ "retain"
1186
+ ] += 1
485
1187
  continue
486
1188
  source = (
487
1189
  selected.record if reaffirm else preserved_record
@@ -499,6 +1201,9 @@ def build_claude_usage_plan(*, selection, desired_events: Iterable[Mapping],
499
1201
  and event_id not in conflicted_event_ids
500
1202
  ):
501
1203
  counts["retain"] += 1
1204
+ counts_by_kind[(desired_record.get("payload") or {})["kind"]][
1205
+ "retain"
1206
+ ] += 1
502
1207
  continue
503
1208
  revision = int(selected.rev) + 1
504
1209
  disposition = "supersede"
@@ -519,6 +1224,13 @@ def build_claude_usage_plan(*, selection, desired_events: Iterable[Mapping],
519
1224
  at = str(desired_record["at"])
520
1225
  payload = dict(desired_record.get("payload") or {})
521
1226
  counts[disposition] += 1
1227
+ kind_record = (
1228
+ current_record if disposition == "tombstone"
1229
+ else desired_record if preserved_record is None
1230
+ else preserved_record
1231
+ )
1232
+ kind = (kind_record.get("payload") or {})["kind"]
1233
+ counts_by_kind[kind][disposition] += 1
522
1234
  action_shape = {
523
1235
  "disposition": disposition,
524
1236
  "eventId": event_id,
@@ -533,9 +1245,10 @@ def build_claude_usage_plan(*, selection, desired_events: Iterable[Mapping],
533
1245
  at=at,
534
1246
  payload=payload,
535
1247
  payload_hash=_sha256(action_shape),
1248
+ kind=kind,
536
1249
  ))
537
1250
 
538
- return RederivePlan(
1251
+ plan = RederivePlan(
539
1252
  family=FAMILY,
540
1253
  journal_high_water=journal_high_water,
541
1254
  cache_fingerprint=cache_fingerprint,
@@ -544,4 +1257,192 @@ def build_claude_usage_plan(*, selection, desired_events: Iterable[Mapping],
544
1257
  actions=tuple(actions),
545
1258
  retained_event_count=retained_event_count,
546
1259
  preserved_event_count=len(preserved),
1260
+ action_counts_by_event_kind=counts_by_kind,
1261
+ )
1262
+ if enforce_guard:
1263
+ enforce_week_reset_add_burst_guard(plan)
1264
+ return plan
1265
+
1266
+
1267
+ def causal_delta_plan(
1268
+ baseline: RederivePlan, reviewed: RederivePlan, *,
1269
+ current_events: Mapping[str, Mapping],
1270
+ authorized_week_reset_origins=frozenset(),
1271
+ ) -> RederivePlan:
1272
+ """Return only C->R actions whose B and R target states differ.
1273
+
1274
+ Both inputs are diffs from the same effective graph C. Therefore an action
1275
+ with the same correction shape in both plans is baseline drift, while a
1276
+ reviewed-only or changed action is caused by the proposed operator record.
1277
+ A baseline-only action means the reviewed target returns to C and requires
1278
+ no correction action.
1279
+ """
1280
+ if (
1281
+ baseline.journal_high_water != reviewed.journal_high_water
1282
+ or baseline.cache_fingerprint != reviewed.cache_fingerprint
1283
+ ):
1284
+ raise RederiveConflict(
1285
+ "baseline and reviewed plans do not share stable inputs"
1286
+ )
1287
+ baseline_by_id = {
1288
+ action.event_id: action.to_correction_action()
1289
+ for action in baseline.actions
1290
+ }
1291
+ reviewed_by_id = {
1292
+ action.event_id: action for action in reviewed.actions
1293
+ }
1294
+ authorized_week_reset_origins = frozenset(
1295
+ authorized_week_reset_origins or ())
1296
+ actions = [
1297
+ action for action in reviewed.actions
1298
+ if (
1299
+ baseline_by_id.get(action.event_id)
1300
+ != action.to_correction_action()
1301
+ or (
1302
+ action.kind == "week_reset"
1303
+ and action.disposition != "tombstone"
1304
+ and (action.payload or {}).get("origin_observation_id")
1305
+ in authorized_week_reset_origins
1306
+ )
1307
+ )
1308
+ ]
1309
+ action_by_id = {action.event_id: action for action in actions}
1310
+ dependency_tombstones = set()
1311
+ for action in actions:
1312
+ if (action.disposition == "tombstone"
1313
+ or action.kind not in _SNAPSHOT_DEPENDENT_KINDS):
1314
+ continue
1315
+ payload = action.payload or {}
1316
+ for field, expected_kind in _milestone_dependencies(
1317
+ action.kind
1318
+ ).items():
1319
+ reference = payload.get(field)
1320
+ if reference in _EMPTY_REFERENCE_SENTINELS:
1321
+ continue
1322
+ dependency_action = action_by_id.get(reference)
1323
+ if (dependency_action is not None
1324
+ and dependency_action.disposition == "tombstone"):
1325
+ current = current_events.get(reference)
1326
+ if ((current or {}).get("payload") or {}).get("kind") \
1327
+ != expected_kind:
1328
+ raise RederiveConflict(
1329
+ "causal milestone dependency cannot be retained: "
1330
+ + action.event_id
1331
+ )
1332
+ dependency_tombstones.add(reference)
1333
+ if dependency_tombstones:
1334
+ actions = [
1335
+ action for action in actions
1336
+ if action.event_id not in dependency_tombstones
1337
+ ]
1338
+
1339
+ target_events = dict(current_events)
1340
+ for action in actions:
1341
+ if action.disposition == "tombstone":
1342
+ target_events.pop(action.event_id, None)
1343
+ else:
1344
+ target_events[action.event_id] = {"payload": action.payload or {}}
1345
+ for action in actions:
1346
+ if (action.disposition == "tombstone"
1347
+ or action.kind not in _SNAPSHOT_DEPENDENT_KINDS):
1348
+ continue
1349
+ payload = action.payload or {}
1350
+ for field, expected_kind in _milestone_dependencies(
1351
+ action.kind
1352
+ ).items():
1353
+ reference = payload.get(field)
1354
+ if reference in _EMPTY_REFERENCE_SENTINELS:
1355
+ continue
1356
+ target = target_events.get(reference)
1357
+ if ((target or {}).get("payload") or {}).get("kind") \
1358
+ != expected_kind:
1359
+ dependency = reviewed_by_id.get(reference)
1360
+ if (
1361
+ dependency is None
1362
+ or dependency.disposition == "tombstone"
1363
+ or dependency.kind != expected_kind
1364
+ or (dependency.payload or {}).get("kind") != expected_kind
1365
+ ):
1366
+ raise RederiveConflict(
1367
+ "causal milestone dependency is missing: "
1368
+ + action.event_id
1369
+ )
1370
+ actions.append(dependency)
1371
+ target_events[reference] = {
1372
+ "payload": dependency.payload or {},
1373
+ }
1374
+ included_ids = {action.event_id for action in actions}
1375
+ actions = tuple(
1376
+ action for action in reviewed.actions
1377
+ if action.event_id in included_ids
547
1378
  )
1379
+ removed = [action for action in reviewed.actions if action not in actions]
1380
+ counts = dict(reviewed.counts)
1381
+ counts_by_kind = {
1382
+ kind: dict(values)
1383
+ for kind, values in reviewed.action_counts_by_event_kind.items()
1384
+ }
1385
+ for action in removed:
1386
+ counts[action.disposition] -= 1
1387
+ counts_by_kind[action.kind][action.disposition] -= 1
1388
+ if action.disposition != "add":
1389
+ counts["retain"] += 1
1390
+ counts_by_kind[action.kind]["retain"] += 1
1391
+ return RederivePlan(
1392
+ family=reviewed.family,
1393
+ journal_high_water=reviewed.journal_high_water,
1394
+ cache_fingerprint=reviewed.cache_fingerprint,
1395
+ config_fingerprint=reviewed.config_fingerprint,
1396
+ counts=counts,
1397
+ actions=actions,
1398
+ retained_event_count=reviewed.retained_event_count,
1399
+ preserved_event_count=reviewed.preserved_event_count,
1400
+ action_counts_by_event_kind=counts_by_kind,
1401
+ )
1402
+
1403
+
1404
+ def week_reset_add_burst_guard(plan: RederivePlan):
1405
+ """Return the unconditional reset-add guard finding for one plan."""
1406
+ # The guard checks the completed deterministic action set, not candidate
1407
+ # observations or retained resets. A second reset of one physical window
1408
+ # remains admissible; three additions for the same exact evidence do not.
1409
+ grouped: dict[tuple, int] = {}
1410
+ for action in plan.actions:
1411
+ payload = action.payload or {}
1412
+ if action.disposition != "add" or payload.get("kind") != "week_reset":
1413
+ continue
1414
+ key = (
1415
+ payload.get("account_key"),
1416
+ payload.get("new_week_end_at"),
1417
+ payload.get("observed_pre_credit_pct"),
1418
+ )
1419
+ if any(value is None for value in key):
1420
+ continue
1421
+ grouped[key] = grouped.get(key, 0) + 1
1422
+ violations = [
1423
+ {
1424
+ "accountKey": account_key,
1425
+ "newWeekEndAt": week_end,
1426
+ "observedPreCreditPct": percent,
1427
+ "addCount": count,
1428
+ }
1429
+ for (account_key, week_end, percent), count in grouped.items()
1430
+ if count > 2
1431
+ ]
1432
+ violations.sort(key=lambda item: (
1433
+ str(item["accountKey"]), str(item["newWeekEndAt"]),
1434
+ repr(item["observedPreCreditPct"]),
1435
+ ))
1436
+ if violations:
1437
+ return {
1438
+ "code": "week-reset-add-burst",
1439
+ "limit": 2,
1440
+ "violations": violations,
1441
+ }
1442
+ return None
1443
+
1444
+
1445
+ def enforce_week_reset_add_burst_guard(plan: RederivePlan) -> None:
1446
+ finding = week_reset_add_burst_guard(plan)
1447
+ if finding is not None:
1448
+ raise RederivePlanGuardConflict(plan, finding)