macrame-db 0.15.0

A Bitemporal Graph Ledger on libSQL · Embedded knowledge database
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
//! What the write actor knows about its own latency (T1.4, D-079).
//!
//! # Why this exists
//!
//! [`crate::CHUNK_BUDGET`] is 3 ms and the crate has, until now, had exactly one
//! way to find out whether that bound holds: run `benches/budgets.rs` on a
//! synthetic fixture. That is a statement about a laptop, not about a database
//! in use. D-059 already established that the bound does **not** hold on a large
//! file, by a factor of 15, and it took a benchmark rewrite to notice — because
//! nothing in the running system was counting.
//!
//! Tier 1's other three items are all "make the tail bounded". None of them can
//! be validated in the field without something that measures the tail, which is
//! why this is a precondition for them rather than a nice-to-have.
//!
//! # What is recorded, and what is deliberately not
//!
//! Four things, all of them per **actor turn** — one command, start to finish:
//!
//! - **queue depth** on both channels, sampled *before* the turn begins;
//! - **hold duration**, bucketed, per command kind;
//! - **holds over budget**, counted separately per kind;
//! - **the longest hold since open**, with the kind that caused it.
//!
//! The hold is the whole turn, not the `execute` call's SQL. That is the
//! quantity the budget is about: the SQLite write lock is not preemptible, so an
//! interactive assertion arriving mid-turn waits for the turn, whatever the turn
//! spent its time on.
//!
//! There is no per-command timestamp trail and no sampling of individual slow
//! commands. That would be a tracing problem, and `tracing` is already a
//! dependency — spans belong there. This module answers one question ("is the
//! bound holding, and if not, which kind breaks it") in fixed memory, with no
//! allocation on the actor's path.
//!
//! # The feature gate
//!
//! Behind `metrics`, which has been a **default** feature since 0.12.11
//! (D-154): a crate whose contract is a latency bound must not ship a default
//! build that cannot report whether the bound is met. `--no-default-features`
//! still removes it. With the feature off, [`ActorMetrics`] is a
//! zero-sized type whose methods compile away and [`HoldTimer::start`] does not
//! read the clock — so the actor loop has **one** shape either way. That
//! matters more than the nanoseconds: a `#[cfg]` in the loop body is how the
//! instrumented and uninstrumented paths drift until only one of them is the one
//! that runs.

use std::time::Duration;

/// The command kinds the actor can spend a turn on.
///
/// One flat enum across both channels rather than one per channel. The question
/// this exists to answer is "which command broke the budget", and a reader
/// looking at a 400 ms hold does not first want to know which queue it came off.
/// Priority is a property of scheduling; kind is a property of cost.
///
/// # `#[non_exhaustive]`, added while it was still free (0.12.8, W4.2)
///
/// Adding a variant here is a **breaking change** without this attribute,
/// because a downstream `match` on `CommandKind` would stop compiling. That is
/// not hypothetical for this enum: [`crate::metrics::CommandKind::Rehydrate`]
/// did not exist until 0.12.9 precisely because adding it was a break, and
/// rehydration reported as `Archive` for several releases as a result. The
/// codebase has already paid this cost once, which is the argument for paying
/// the attribute now rather than deciding it at 1.0 when the cost is permanent.
///
/// Callers must therefore include a `_ =>` arm. In exchange, this enum can grow
/// a variant for a command kind that does not exist yet without a major version.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(u8)]
#[non_exhaustive]
pub enum CommandKind {
    AssertEdge,
    RetireEdge,
    UpsertConcept,
    WriteBulkAtomic,
    RebuildCurrent,
    RegisterModel,
    Shutdown,
    BulkImportChunk,
    WriteConceptsChunk,
    WriteAnalyticsChunk,
    UpsertEmbeddingChunk,
    Archive,
    RebuildFts,
    /// The **fill** half of a chunked shadow rebuild — `Begin` and every
    /// `Fill` chunk (T1.2).
    ///
    /// Its own kind rather than folded into `RebuildCurrent`, because the two
    /// have opposite latency profiles and the whole point of the chunked path
    /// is that its turns are short — averaging them together would hide
    /// exactly the improvement.
    ///
    /// **Fill-only since 0.14.16** ([D-233]). Through 0.14.15 this kind also
    /// carried the swap turn, which is over budget by construction, so its
    /// `over_budget` count was `N(rebuilds) + regressions` and could not be
    /// decomposed — the counter was a constant, not a signal. The swap is
    /// [`CommandKind::ShadowSwap`] now, and what is left here is the half that
    /// is *meant* to fit [`crate::CHUNK_BUDGET`]. A nonzero count on this kind
    /// is therefore a clean canary: a fill chunk ran long, which is a
    /// regression and nothing else.
    ///
    /// [D-233]: ../docs/architecture/s13-decision-register.md#d-233
    ShadowRebuild,
    /// Refreshing the query planner's statistics (0.12.4, D-149).
    ///
    /// Its own kind rather than folded into `RebuildFts`, though both are
    /// maintenance on derived state: this one is bounded by
    /// `PRAGMA analysis_limit` and that one is bounded by the size of the
    /// concept table, so averaging their holds together would describe neither.
    Analyze,
    /// Moving archived rows back into the hot file (0.12.9, W4.3, D-152).
    ///
    /// Its own kind at last. Through 0.12.8 this reported as
    /// [`CommandKind::Archive`], on the stated ground that rehydration is the
    /// archive path run backwards and shares its budget — true of the *budget*
    /// and false of the *attribution*, which is what a metrics surface is for.
    /// An operator reading a long `archive` hold could not tell whether the
    /// database had archived anything at all, and the two move rows in opposite
    /// directions.
    ///
    /// The real reason it stayed folded was that adding a variant was a
    /// breaking change. `#[non_exhaustive]` (W4.2) is what removed that
    /// obstacle, and this variant is the first thing it bought — which is also
    /// the evidence that the attribute was worth adding rather than a
    /// precaution against a hypothetical.
    ///
    /// **Appended at the end**, per [`CommandKind::index`]: the position of
    /// every existing variant is a persisted contract in two languages.
    Rehydrate,
    /// An explicit `PRAGMA wal_checkpoint` (0.12.13, W5.2, D-156).
    ///
    /// Its own kind because it is the one actor turn that is **not** a
    /// transaction: it moves frames from the WAL back into the main database
    /// file, and its duration is a function of how much WAL has accumulated
    /// rather than of anything the caller passed. Folding it into any existing
    /// kind would make that kind's hold distribution bimodal for a reason no
    /// dashboard could recover.
    ///
    /// **Appended at the end**, per [`CommandKind::index`].
    Checkpoint,
    /// `PRAGMA optimize` — re-analysing only what SQLite believes has drifted
    /// (0.13.24, W10.5, D-197).
    ///
    /// Split out of [`CommandKind::Analyze`], which covered both from 0.12.4 to
    /// 0.13.23. The split is [`CommandKind::Rehydrate`]'s lesson applied before
    /// the fact rather than after it: [D-168] refused to decide `Analyze`'s
    /// budget exemption *because* the kind was shared, since a judgement made
    /// about the explicit call would have landed on the automatic one —
    /// `close()` runs `optimize()` unconditionally — without ever being made
    /// about it.
    ///
    /// The two also have genuinely different hold distributions, which is the
    /// same argument [`CommandKind::ShadowRebuild`] is separate on.
    /// [`crate::Database::analyze`] does the work unconditionally and its hold
    /// tracks the table. This one is a no-op when nothing has moved, so its
    /// distribution is bimodal by design and averaging the two together
    /// describes neither.
    ///
    /// **Appended at the end**, per [`CommandKind::index`].
    ///
    /// [D-168]: ../docs/architecture/s13-decision-register.md#d-168
    Optimize,
    /// Registering a lineage (0.14.7, §15.4).
    ///
    /// Its own kind rather than folded into `AssertEdge`, though both are one
    /// small transaction: a fork writes to `branches` and nothing else, so its
    /// hold is the floor an actor turn can have, and averaging it into a
    /// command that touches four tables would flatter that command's numbers.
    ///
    /// Last in declaration order because that order is a persisted contract and
    /// **new variants go at the end** — see [`CommandKind::index`]. Grouping it
    /// next to `RegisterModel`, which is where it belongs by kind, would have
    /// renumbered nine counters and relabelled the Python histogram's axes.
    Fork,
    /// Forgetting a lineage (0.14.13, §15.4, D-230).
    ///
    /// Its own kind rather than folded into [`CommandKind::Archive`], on
    /// [D-152]'s finding rather than on a fresh argument: the budget really is
    /// shared and the attribution is not, and an operator reading a long
    /// `archive` hold could not tell whether the database had archived a
    /// backlog of closed intervals or dropped an abandoned branch. The two also
    /// have unrelated cost curves — one is a function of how long it has been
    /// since the last run, the other of how much was written on one branch.
    ///
    /// At the end of the declaration order, per [`CommandKind::index`].
    ///
    /// [D-152]: ../docs/architecture/s13-decision-register.md#d-152
    ArchiveBranch,
    /// The **swap** turn of a chunked shadow rebuild (0.14.16, D-233).
    ///
    /// Split out of [`CommandKind::ShadowRebuild`], which covered both halves
    /// from 0.6.0 to 0.14.15. This is the third instance of one shape —
    /// [`CommandKind::Rehydrate`] out of `Archive` ([D-152]),
    /// [`CommandKind::Optimize`] out of `Analyze` ([D-197]), this — so the
    /// class is named where it can be seen: **one `CommandKind`, one
    /// structural hold distribution.** A kind covering two is a defect on
    /// arrival, to be split in review rather than found by probe.
    ///
    /// Here the bimodality is structural rather than workload-dependent, which
    /// is what makes it the clearest instance of the three. Index names are
    /// global and SQLite has no `ALTER INDEX … RENAME`, so the shadow cannot
    /// carry `idx_lc_traversal_cover` while the live table still holds that
    /// name — the swap is where all three indexes get built, under the write
    /// lock. [D-082](../docs/architecture/s13-decision-register.md#d-082)
    /// measured it at **46.8 ms**, 15.6× the budget, and it grows with the
    /// table.
    ///
    /// **Exempt**, unlike its fill half — see
    /// [`CommandKind::exempt_from_budget`], where the criterion is stated.
    ///
    /// At the end of the declaration order, per [`CommandKind::index`].
    ///
    /// [D-197]: ../docs/architecture/s13-decision-register.md#d-197
    ShadowSwap,
}

impl CommandKind {
    /// Every kind, in declaration order. Indexing into the per-kind arrays is by
    /// position in this slice, so the two must not drift — which is why the
    /// arrays are sized from `ALL.len()` rather than from a hand-written count.
    pub const ALL: &'static [CommandKind] = &[
        CommandKind::AssertEdge,
        CommandKind::RetireEdge,
        CommandKind::UpsertConcept,
        CommandKind::WriteBulkAtomic,
        CommandKind::RebuildCurrent,
        CommandKind::RegisterModel,
        CommandKind::Shutdown,
        CommandKind::BulkImportChunk,
        CommandKind::WriteConceptsChunk,
        CommandKind::WriteAnalyticsChunk,
        CommandKind::UpsertEmbeddingChunk,
        CommandKind::Archive,
        CommandKind::RebuildFts,
        CommandKind::ShadowRebuild,
        CommandKind::Analyze,
        CommandKind::Rehydrate,
        CommandKind::Checkpoint,
        CommandKind::Optimize,
        CommandKind::Fork,
        CommandKind::ArchiveBranch,
        CommandKind::ShadowSwap,
    ];

    pub const COUNT: usize = CommandKind::ALL.len();

    /// This kind's slot in the per-kind arrays.
    ///
    /// # Declaration order is a persisted contract (0.12.8, W4.2)
    ///
    /// `self as usize` means the **order of the variants above** is the order of
    /// every per-kind array in this module, and the compiler cannot catch a
    /// change to it. Reordering the enum silently reassigns every counter to a
    /// different command: the code compiles, the tests pass, and a histogram
    /// read after the change attributes `archive`'s holds to `rebuild_fts`.
    ///
    /// **New variants go at the end**, always — including at the end of
    /// [`CommandKind::ALL`], whose order is what `as_str()` and the Python
    /// surface enumerate. This binds Python too: `BUCKET_BOUNDS_MICROS` is a
    /// module constant there and `KindMetrics` is built by position, so a
    /// reorder here relabels axes in a language the Rust compiler is not
    /// looking at.
    ///
    /// `#[repr(u8)]` is on the enum for the same reason — it pins the
    /// discriminants to the declaration order rather than leaving them to the
    /// compiler — but it pins them to whatever the order *is*, so it does not
    /// make a reorder safe. Only this rule does.
    pub const fn index(self) -> usize {
        self as usize
    }

    pub const fn as_str(self) -> &'static str {
        match self {
            CommandKind::AssertEdge => "assert_edge",
            CommandKind::RetireEdge => "retire_edge",
            CommandKind::UpsertConcept => "upsert_concept",
            CommandKind::WriteBulkAtomic => "write_bulk_atomic",
            CommandKind::RebuildCurrent => "rebuild_current",
            CommandKind::RegisterModel => "register_model",
            CommandKind::Shutdown => "shutdown",
            CommandKind::BulkImportChunk => "bulk_import_chunk",
            CommandKind::WriteConceptsChunk => "write_concepts_chunk",
            CommandKind::WriteAnalyticsChunk => "write_analytics_chunk",
            CommandKind::UpsertEmbeddingChunk => "upsert_embedding_chunk",
            CommandKind::Archive => "archive",
            CommandKind::RebuildFts => "rebuild_fts",
            CommandKind::ShadowRebuild => "shadow_rebuild",
            CommandKind::Analyze => "analyze",
            CommandKind::Rehydrate => "rehydrate",
            CommandKind::Checkpoint => "checkpoint",
            CommandKind::Optimize => "optimize",
            CommandKind::Fork => "fork",
            CommandKind::ArchiveBranch => "archive_branch",
            CommandKind::ShadowSwap => "shadow_swap",
        }
    }

    /// Whether this kind is exempt from [`crate::CHUNK_BUDGET`] by contract.
    ///
    /// The exemptions are the table in `CHUNK_BUDGET`'s own rustdoc, and they
    /// are carried here so a dashboard can separate "the budget is being
    /// broken" from "the budget does not apply and never claimed to". Counting
    /// an `archive` as a budget violation would make the violation count useless
    /// on any database that archives.
    ///
    /// The two lists must agree, and since 0.12.9 they are tied together in
    /// both directions by `the_budget_exemptions_and_their_documented_table_agree`
    /// — the extra-row direction being the one worth having, since a table row
    /// with no code behind it promises a caller an exemption the violation
    /// counter is about to disagree with.
    ///
    /// # The criterion, stated at last (0.14.16, W12.16, [D-233])
    ///
    /// The register applied one rule three times without naming it, and naming
    /// it is what let the fourth case be decided rather than argued.
    ///
    /// > **Exempt means the chunk bound does not apply: the operation is atomic
    /// > by necessity and has no smaller unit. Counted means the bound applies,
    /// > so exceeding it is information.**
    ///
    /// Every exemption on this list was argued that way in its own release,
    /// whatever the summary sentence said afterwards.
    /// [`CommandKind::WriteBulkAtomic`] ([D-014]) is one statement, and is the
    /// kind that exists precisely because the chunked variant is *not* atomic.
    /// [`CommandKind::Archive`] ([D-012]) and [`CommandKind::ArchiveBranch`]
    /// delete a consistent set or none of it.
    /// [`CommandKind::RebuildCurrent`] ([D-023]) re-derives a whole projection
    /// in one transaction. [`CommandKind::Rehydrate`] ([D-152]) is one
    /// unchunked transaction moving rows back across the file boundary.
    /// [`CommandKind::Checkpoint`] ([D-156]) is a WAL boundary. None of them
    /// has a smaller unit to chunk *into*, so 3 ms is not a bound they failed —
    /// it is a bound that was never about them.
    /// [`CommandKind::Analyze`] and [`CommandKind::Optimize`] ([D-197]) do have
    /// one: they are bounded work that can take longer or shorter, so exceeding
    /// is a fact about this database and worth counting.
    ///
    /// # The criterion took three tries, and the two that failed are the useful part
    ///
    /// **v1 — *expected-on-healthy is exempt, workload-dependent is not*.**
    /// Falsified by reading [D-197] closely rather than by any new measurement:
    /// `Optimize` **runs on every close** and stays counted. If expectedness
    /// decided the question, `Optimize` would be exempt and it is not.
    ///
    /// **v2 — *if `over_budget` can differ from `turns`, count it*.** Falsified
    /// by three of the exemptions themselves: an `Archive` with nothing
    /// archivable, a `Rehydrate` of a single row and a `Checkpoint` on an empty
    /// WAL all come in *under* budget, so their counters can differ from their
    /// turn counts and the rule would un-exempt all three.
    ///
    /// Both were **observational** — read off the outcomes the existing
    /// exemptions happened to produce, and so decidable only after the fact.
    /// Inapplicability is decidable at design time from what the operation *is*,
    /// which is what a criterion has to be if it is to settle the next case
    /// rather than rationalise the last one.
    ///
    /// Expected-on-healthy survives as **corroboration, not definition**: a kind
    /// with no smaller unit usually does exceed on every healthy database, so
    /// the symptom is a fair sanity check on the diagnosis. `Optimize` is
    /// exactly the case that shows why it cannot be the test itself.
    ///
    /// `over_budget` is incremented once per turn that exceeds, so it counts
    /// **occurrences and not magnitude**. That is the fact the criterion turns
    /// on: a kind whose every turn exceeds contributes a constant to
    /// [`MetricsSnapshot::budget_violations`] and moves not at all when the
    /// hold doubles. Growth is visible in this kind's histogram and
    /// [`KindSnapshot::longest`], which no exemption touches.
    ///
    /// # The two halves of a shadow rebuild land on opposite sides of it
    ///
    /// [`CommandKind::ShadowSwap`] is exempt: ≥ 15.6× by construction ([D-082]
    /// measured 46.8 ms against a 3 ms budget), with no healthy state in which
    /// it fits, and routine — the crate's own end-to-end suite triggers one.
    /// Counting it would put a permanent `N(rebuilds)` in the violation list
    /// of every database that has ever repaired its projection, which is
    /// [`CommandKind::Rehydrate`]'s argument exactly.
    ///
    /// [`CommandKind::ShadowRebuild`] — the fill half — is **not** exempt, and
    /// that is the half [D-082] was protecting when it refused to exempt the
    /// merged kind: *"exempting the kind would hide the first fact to excuse
    /// the second."* The goal is reaffirmed and the mechanism superseded. The
    /// split protects fill structurally, where non-exemption of the merged
    /// kind only protected it in principle: a fill regression used to arrive
    /// as `+1` on a counter that already read `N(rebuilds)`, and now it is the
    /// only thing that can move `shadow_rebuild` off zero at all.
    ///
    /// `a_swap_over_budget_is_not_a_violation` is what keeps this honest, and
    /// its fixture is the load-bearing part: it seeds enough of a graph to put
    /// the swap **over** the budget, asserts that first, and only then asserts
    /// the swap's own count is zero. The obvious form — run a rebuild, assert
    /// the violation list is empty — is worthless twice over. On a small
    /// fixture the swap finishes inside 3 ms and the assertion passes whether
    /// the kind is exempt or not; on a real one the *fill* chunks exceed the
    /// budget legitimately (3.14 ms at 200 keys in a debug build), so an empty
    /// list is a property of small fixtures rather than of rebuilds.
    ///
    /// The two tests are **one instrument with two asymmetric halves**, and it
    /// is worth being exact about which owns what.
    /// `a_swap_over_budget_is_not_a_violation` owns **narrowing**: re-count the
    /// swap and its assertion moves off zero. It cannot own widening, because
    /// widening an exemption only ever *removes* entries from
    /// [`MetricsSnapshot::budget_violations`] — an assertion that a count is
    /// zero stays green under every widening, including one that swallows the
    /// fill half whole. **Widening is owned by
    /// `a_long_fill_is_a_violation_and_a_long_swap_is_not` below and by nothing
    /// else**, because forging a long fill and asserting it **is** counted is
    /// the only shape of assertion a widening can break.
    ///
    /// # Any claim about fill and this budget must name a build mode and a fixture size
    ///
    /// At fixture scale the 3 ms bound sits **inside** fill variance rather
    /// than above it, so the same assertion is true or false depending on how
    /// the binary was compiled and how much graph it was handed. Debug
    /// especially: 200 keys × 4 generations puts the longest fill at 3.14 ms —
    /// one violation, the counter working — while a release build of the same
    /// shape stays under. A test that asserts anything about fill overages is
    /// therefore asserting something about *its own fixture and profile*, and
    /// has to say which. The swap is the opposite and that is why the exemption
    /// is testable at all: it exceeds by 15.6× and it exceeds in every mode.
    ///
    /// [D-012]: ../docs/architecture/s13-decision-register.md#d-012
    /// [D-014]: ../docs/architecture/s13-decision-register.md#d-014
    /// [D-023]: ../docs/architecture/s13-decision-register.md#d-023
    /// [D-082]: ../docs/architecture/s13-decision-register.md#d-082
    /// [D-152]: ../docs/architecture/s13-decision-register.md#d-152
    /// [D-156]: ../docs/architecture/s13-decision-register.md#d-156
    /// [D-197]: ../docs/architecture/s13-decision-register.md#d-197
    /// [D-233]: ../docs/architecture/s13-decision-register.md#d-233
    ///
    /// # `Rehydrate` is exempt, and splitting it out is what made that a
    /// decision rather than an accident (0.12.9, W4.3, D-152)
    ///
    /// Until 0.12.8 rehydration reported as [`CommandKind::Archive`] and was
    /// therefore exempt **by inheritance** — nobody had decided it, it fell out
    /// of the borrowed kind. Giving it its own variant would have silently
    /// flipped it to non-exempt, and since a rehydrate is one unchunked
    /// transaction moving rows back across the file boundary, every single one
    /// would have counted as a budget violation. The violation count would then
    /// have become useless on any database that rehydrates, which is precisely
    /// the failure the `Archive` exemption exists to prevent, arriving by the
    /// back door of a change made for attribution.
    ///
    /// So it is exempt, on the merits and now on the record: rehydration is the
    /// archive path run backwards and makes the same claim about its hold —
    /// that it is bulk movement with no latency bound, and that the caller asked
    /// for it explicitly.
    ///
    /// # Neither [`CommandKind::Analyze`] nor [`CommandKind::Optimize`] is
    /// exempt, and since 0.13.24 those are two decisions (W10.5, D-197)
    ///
    /// They were one kind from 0.12.4 to 0.13.23, and [D-168] declined to decide
    /// the exemption *because* they were: `Analyze` covered
    /// [`crate::Database::optimize`] too, `close()` calls that unconditionally,
    /// and so a judgement made about the explicit call would have landed on the
    /// automatic one without ever being made about it. That is
    /// [`CommandKind::Rehydrate`]'s lesson above arriving from the other
    /// direction — there a shared kind *granted* an exemption nobody had
    /// decided; here one would have *laundered* one. W10.5 split the kind so
    /// each could be answered on its own evidence. Both answers came back the
    /// same and the reasons are different, which is the whole reason the split
    /// had to come first.
    ///
    /// **[`CommandKind::Analyze`] cannot state a `Bound`, so it cannot have a
    /// row.** `ANALYZE` is one indivisible statement whose cost is set by data
    /// volume — measured at **5.26 ms at 10,000 edges and 19.1 ms at 40,000**
    /// against a 3 ms budget (`examples/analyze_hold.rs`, [D-166]). Every call
    /// is a violation and always will be. `Checkpoint`'s bound is frames
    /// accumulated since the last one; `Archive`'s is the session's row count.
    /// The honest entry here would be "the size of the table, damped 3–4× by
    /// `analysis_limit`", which is not a bound but the absence of one, and a
    /// row that cannot fill that column is this table admitting the thing it
    /// exists to prevent.
    ///
    /// **[`CommandKind::Optimize`] is not exempt for the opposite reason: its
    /// violations are rare and they are the informative ones.** Measured
    /// (`examples/optimize_hold.rs`, 40,000 edges): **10.7 ms the first time on
    /// a database that has never been analysed, and 90–220 µs every time
    /// after** — comfortably inside the budget, including immediately after a
    /// bulk load that doubled the ledger. It is over budget only when it
    /// actually re-analyses something, and then it is over by a lot: **460 ms**
    /// once the table had grown 25× and SQLite's staleness ratio finally
    /// fired. So the count is bimodal by construction and it is *reporting*
    /// rather than complaining: an `optimize` in `budget_violations()` marks
    /// the calls that did work, which is exactly what an operator wants to
    /// know and exactly what exempting the kind would delete.
    ///
    /// **The violations are expected and must not be "fixed" by lowering
    /// [`crate::schema::ddl::ANALYSIS_LIMIT`].** That would buy the number by
    /// sampling too little to separate the two `source_id`-leading indices,
    /// which is the entire purpose of having statistics ([D-149]).
    ///
    /// [D-149]: ../docs/architecture/s13-decision-register.md#d-149
    /// [D-166]: ../docs/architecture/s13-decision-register.md#d-166
    /// [D-168]: ../docs/architecture/s13-decision-register.md#d-168
    pub const fn exempt_from_budget(self) -> bool {
        matches!(
            self,
            CommandKind::WriteBulkAtomic
                | CommandKind::Archive
                | CommandKind::RebuildCurrent
                | CommandKind::Rehydrate
                | CommandKind::ArchiveBranch
                | CommandKind::Checkpoint
                | CommandKind::ShadowSwap
        )
    }
}

impl std::fmt::Display for CommandKind {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(self.as_str())
    }
}

/// Upper bounds of the hold-duration histogram, in microseconds.
///
/// `3_000` is [`crate::CHUNK_BUDGET`] exactly, so the bucket boundary and the
/// bound are the same number and a reader does not have to interpolate to answer
/// "what fraction of turns fit". The tail runs to 1 s because D-059's measured
/// worst case was 45 ms and `rebuild_current` at 40K rows is 318 ms (D-077) —
/// a range this has to cover without saturating.
///
/// Anything above the last bound lands in the overflow bucket, which is why
/// [`KindSnapshot::buckets`] is one longer than this slice.
pub const BUCKET_BOUNDS_MICROS: &[u64] = &[
    100, 300, 1_000, 3_000, 10_000, 30_000, 100_000, 300_000, 1_000_000,
];

/// Number of histogram buckets, including the overflow bucket.
pub const BUCKET_COUNT: usize = BUCKET_BOUNDS_MICROS.len() + 1;

#[allow(dead_code)] // used by `imp` under `metrics`, and by the tests always
fn bucket_of(micros: u64) -> usize {
    // Linear scan over nine bounds. A binary search here would be slower in
    // practice and this runs once per actor turn, against a turn measured in
    // microseconds at best.
    BUCKET_BOUNDS_MICROS
        .iter()
        .position(|&bound| micros <= bound)
        .unwrap_or(BUCKET_BOUNDS_MICROS.len())
}

/// Times one actor turn.
///
/// # This clock is no longer optional (0.12.0, W1)
///
/// Until 0.11.0 the field was `#[cfg(feature = "metrics")]` and `elapsed()`
/// returned `Duration::ZERO` in a default build: the reading existed only to
/// feed [`ActorMetrics::record_hold`]'s histogram, so a build that did not keep
/// the histogram had no reason to read a clock.
///
/// The chunk loop changed what the reading is *for*. A chunk's measured hold is
/// now the input to the next chunk's size (`connection::next_chunk_size`, named
/// in prose because it is private — D-144), which means it is a control signal
/// in every build and not an observation in some of them. Left gated, `bulk_import` would have sized its chunks off
/// `Duration::ZERO` — a value that reads as "comfortably under budget" — and
/// grown every chunk to the ceiling, in exactly the builds nobody was measuring.
///
/// So the clock is unconditional and **only the histogram is still gated**:
/// `record_hold` remains a no-op without the feature. What that costs is one
/// `Instant::now()` pair per actor turn — tens of nanoseconds against a turn
/// measured in microseconds at best, and the same reasoning §5.1.5 uses to
/// decide that a channel hop is free beside a chunk.
///
/// It stays a type rather than a bare `Instant::now()` in the loop because the
/// ordering guarantee in [`crate::connection`]'s `Turn` is attached to it.
pub struct HoldTimer {
    start: std::time::Instant,
}

impl HoldTimer {
    #[inline]
    pub fn start() -> Self {
        Self {
            start: std::time::Instant::now(),
        }
    }

    #[inline]
    pub fn elapsed(&self) -> Duration {
        self.start.elapsed()
    }
}

// ---------------------------------------------------------------------------
// Instrumented implementation
// ---------------------------------------------------------------------------

#[cfg(feature = "metrics")]
mod imp {
    use super::{bucket_of, CommandKind, BUCKET_COUNT};
    use std::sync::atomic::{AtomicU64, Ordering};
    use std::time::Duration;

    /// One kind's counters. All `Relaxed`: these are statistics, and ordering
    /// them against each other would buy a consistency no reader needs and cost
    /// fences on the write path.
    #[derive(Debug, Default)]
    struct Kind {
        turns: AtomicU64,
        total_micros: AtomicU64,
        over_budget: AtomicU64,
        /// This kind's own high-water mark, in µs.
        ///
        /// Not redundant with the global `longest`. That one names a single
        /// command, so on any real database it names whichever kind is slowest
        /// overall — and the question "did windowing shrink the archive's worst
        /// hold" cannot be answered by a counter that a bulk import wins. No
        /// packing needed here: the kind is the array index.
        longest_micros: AtomicU64,
        buckets: [AtomicU64; BUCKET_COUNT],
    }

    /// Live counters, shared between the actor and the handle.
    ///
    /// Fixed size, no allocation, no lock. The actor updates; anyone may read.
    #[derive(Debug, Default)]
    pub struct ActorMetrics {
        kinds: [Kind; CommandKind::COUNT],
        /// Packed `micros << 8 | kind`, so the longest hold and the kind that
        /// caused it are read and written as **one** value. Two atomics would
        /// let a reader see a duration from one turn beside a kind from
        /// another — a rare wrong answer to exactly the question this field
        /// exists to answer. 2^56 µs is over two thousand years.
        ///
        /// **The duration must occupy the high bits.** The update is a
        /// `fetch_max` on the packed word, so whichever field is packed high is
        /// the one being compared. The first version of this had the kind up
        /// there, which made the "longest hold" the hold with the largest
        /// *enum index* — a 3 ms `write_concepts_chunk` beat a 10 ms
        /// `rebuild_current` because its variant is declared later. It was
        /// `actor_metrics_tests` that caught it, not the unit tests, because
        /// nothing in the arithmetic is wrong: the packing is only incorrect in
        /// the presence of the atomic operation it exists to serve.
        longest: AtomicU64,
        /// Loop iterations, which is **not** the number of turns taken.
        ///
        /// The depth sample happens at the top of the loop, before `select!`
        /// blocks — so an idle actor has already counted the iteration for a
        /// command that has not arrived. That is right for depth (the sample is
        /// "what was queued when I went looking") and wrong for turns, which is
        /// why [`MetricsSnapshot::turns`] is the sum of the per-kind counters
        /// instead. Conflating the two made `turns` permanently one too high and
        /// disagree with its own breakdown.
        depth_samples: AtomicU64,
        high_depth_sum: AtomicU64,
        high_depth_max: AtomicU64,
        low_depth_sum: AtomicU64,
        low_depth_max: AtomicU64,
        /// Turns where the actor took high-priority work while low-priority
        /// work was already queued (0.12.10, W4.4, D-153).
        ///
        /// The `biased` `select!` in `run_writer_actor` has **no floor**:
        /// sustained high-priority traffic can hold the low tier off
        /// indefinitely, and nothing has ever said whether that happens. This
        /// is the numerator of that question — how often the choice went
        /// against the low tier at all.
        low_starved_turns: AtomicU64,
        /// The current unbroken run of such turns. Reset to zero the moment
        /// low-priority work is taken.
        ///
        /// Not exposed; it is the state [`Self::low_starved_run_max`] is a
        /// high-water mark of. A live value would be read at an arbitrary point
        /// in a run and mean nothing.
        low_starved_run: AtomicU64,
        /// The longest such run since open, which is the number that answers the
        /// question.
        ///
        /// A large `low_starved_turns` on a busy database is unremarkable — it
        /// says the high tier is being used, which is what the tier is for. A
        /// large *run* says one specific low-priority command waited that many
        /// turns, and it is the only one of the two that can distinguish
        /// "prioritised" from "starved".
        low_starved_run_max: AtomicU64,
    }

    const MICROS_SHIFT: u32 = 8;
    const KIND_MASK: u64 = (1 << MICROS_SHIFT) - 1;

    impl ActorMetrics {
        pub fn new() -> Self {
            Self::default()
        }

        /// Sample both queue depths. Called before the turn, not after: after
        /// the turn the queue reflects what arrived *during* it, which is a
        /// different and much less useful quantity.
        #[inline]
        pub fn record_turn(&self, high_depth: usize, low_depth: usize) {
            self.depth_samples.fetch_add(1, Ordering::Relaxed);
            for (sum, max, depth) in [
                (
                    &self.high_depth_sum,
                    &self.high_depth_max,
                    high_depth as u64,
                ),
                (&self.low_depth_sum, &self.low_depth_max, low_depth as u64),
            ] {
                sum.fetch_add(depth, Ordering::Relaxed);
                max.fetch_max(depth, Ordering::Relaxed);
            }
        }

        /// Record which tier the `select!` chose, and what was waiting.
        ///
        /// `low_queued` is the depth sampled *before* the `select!`, so it is
        /// the backlog the turn found on arrival. By the time a high-priority
        /// arm fires the low queue may have grown; using the pre-select reading
        /// keeps this consistent with every other depth figure in this module
        /// and makes the counter conservative — it never invents starvation
        /// from work that arrived after the choice was made.
        ///
        /// A low-priority turn resets the run rather than decrementing it: the
        /// question is "how many turns did one low-priority command wait", and
        /// that is a run length, not a balance.
        #[inline]
        pub fn record_priority_choice(&self, took_high: bool, low_queued: usize) {
            if took_high && low_queued > 0 {
                self.low_starved_turns.fetch_add(1, Ordering::Relaxed);
                let run = self.low_starved_run.fetch_add(1, Ordering::Relaxed) + 1;
                self.low_starved_run_max.fetch_max(run, Ordering::Relaxed);
            } else if !took_high {
                self.low_starved_run.store(0, Ordering::Relaxed);
            }
        }

        #[inline]
        pub fn record_hold(&self, kind: CommandKind, held: Duration) {
            let micros = held.as_micros().min(super::MICROS_CEILING as u128) as u64;
            let k = &self.kinds[kind.index()];
            k.turns.fetch_add(1, Ordering::Relaxed);
            k.total_micros.fetch_add(micros, Ordering::Relaxed);
            k.buckets[bucket_of(micros)].fetch_add(1, Ordering::Relaxed);
            k.longest_micros.fetch_max(micros, Ordering::Relaxed);
            if !kind.exempt_from_budget() && held > crate::CHUNK_BUDGET {
                k.over_budget.fetch_add(1, Ordering::Relaxed);
            }
            self.longest.fetch_max(
                (micros << MICROS_SHIFT) | kind.index() as u64,
                Ordering::Relaxed,
            );
        }

        /// A consistent-enough picture for a dashboard.
        ///
        /// Not a torn-read-free snapshot, and it does not pretend to be: the
        /// actor keeps running while this walks the array, so two kinds may be
        /// read one turn apart. Locking the actor to produce a report would make
        /// the observer a source of the latency it is measuring.
        pub fn snapshot(&self) -> super::MetricsSnapshot {
            let samples = self.depth_samples.load(Ordering::Relaxed);
            let mean = |sum: &AtomicU64| {
                if samples == 0 {
                    0.0
                } else {
                    sum.load(Ordering::Relaxed) as f64 / samples as f64
                }
            };

            let packed = self.longest.load(Ordering::Relaxed);
            let longest_micros = packed >> MICROS_SHIFT;
            let longest = (longest_micros > 0)
                .then(|| {
                    let idx = (packed & KIND_MASK) as usize;
                    CommandKind::ALL
                        .get(idx)
                        .map(|&kind| (kind, Duration::from_micros(longest_micros)))
                })
                .flatten();

            let kinds: Vec<_> = CommandKind::ALL
                .iter()
                .map(|&kind| {
                    let k = &self.kinds[kind.index()];
                    let turns = k.turns.load(Ordering::Relaxed);
                    let total = k.total_micros.load(Ordering::Relaxed);
                    super::KindSnapshot {
                        kind,
                        turns,
                        over_budget: k.over_budget.load(Ordering::Relaxed),
                        mean: total
                            .checked_div(turns)
                            .map_or(Duration::ZERO, Duration::from_micros),
                        longest: Duration::from_micros(k.longest_micros.load(Ordering::Relaxed)),
                        buckets: std::array::from_fn(|i| k.buckets[i].load(Ordering::Relaxed)),
                    }
                })
                .collect();

            super::MetricsSnapshot {
                // Summed, not counted separately — see `depth_samples`.
                turns: kinds.iter().map(|k| k.turns).sum(),
                depth_samples: samples,
                high_depth_mean: mean(&self.high_depth_sum),
                high_depth_max: self.high_depth_max.load(Ordering::Relaxed),
                low_depth_mean: mean(&self.low_depth_sum),
                low_depth_max: self.low_depth_max.load(Ordering::Relaxed),
                low_starved_turns: self.low_starved_turns.load(Ordering::Relaxed),
                low_starved_run_max: self.low_starved_run_max.load(Ordering::Relaxed),
                longest,
                kinds,
            }
        }
    }
}

// ---------------------------------------------------------------------------
// No-op implementation
// ---------------------------------------------------------------------------

#[cfg(not(feature = "metrics"))]
mod imp {
    use super::CommandKind;
    use std::time::Duration;

    /// The `metrics`-off shape: zero-sized, and every method is nothing.
    #[derive(Debug, Default)]
    pub struct ActorMetrics;

    impl ActorMetrics {
        pub fn new() -> Self {
            Self
        }
        #[inline]
        pub fn record_turn(&self, _high_depth: usize, _low_depth: usize) {}
        #[inline]
        pub fn record_priority_choice(&self, _took_high: bool, _low_queued: usize) {}
        #[inline]
        pub fn record_hold(&self, _kind: CommandKind, _held: Duration) {}
    }
}

pub use imp::ActorMetrics;

/// Saturation point for a recorded hold, in microseconds (~2,000 years).
///
/// Exists so the packed `longest` field cannot have a pathological duration
/// overflow into the kind bits. A hold this long is not a measurement, it is a
/// hang — and the counter should stay readable rather than start reporting the
/// wrong command.
///
/// Kept out of the `metrics` cfg so the invariant test below runs in the default
/// build too: the packing is a property of the layout, and a build that does not
/// record is exactly the build where nobody would notice it break.
#[allow(dead_code)]
const MICROS_CEILING: u64 = (1u64 << 56) - 1;

/// One command kind's holds, as of the moment [`ActorMetrics::snapshot`] read it.
#[cfg(feature = "metrics")]
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct KindSnapshot {
    pub kind: CommandKind,
    /// Turns spent on this kind.
    pub turns: u64,
    /// Turns that exceeded [`crate::CHUNK_BUDGET`]. Always 0 for the kinds
    /// [`CommandKind::exempt_from_budget`] names — see there for why, and for
    /// the criterion that decides which those are.
    ///
    /// **Occurrences, not magnitude**: one per turn that exceeded, however far
    /// it exceeded by. A kind whose hold has doubled reports the same count and
    /// a different [`Self::longest`].
    pub over_budget: u64,
    pub mean: Duration,
    /// This kind's longest hold. Distinct from [`MetricsSnapshot::longest`],
    /// which names one command across all kinds and so tends to be permanently
    /// whichever kind is slowest overall.
    pub longest: Duration,
    /// Counts per [`BUCKET_BOUNDS_MICROS`], plus a final overflow bucket.
    ///
    /// Private behind [`Self::buckets`] since 0.12.8 (W4.2). A public array
    /// field publishes `BUCKET_COUNT` as part of the type's shape, so adding a
    /// bucket bound would break every caller that named the length — and the
    /// bounds are exactly the thing a latency histogram is likely to want to
    /// re-cut. The accessor returns a slice and the length becomes an
    /// observation rather than a signature. Python already did it this way.
    buckets: [u64; BUCKET_COUNT],
}

#[cfg(feature = "metrics")]
impl KindSnapshot {
    /// Counts per [`BUCKET_BOUNDS_MICROS`], plus a final overflow bucket.
    ///
    /// Pair it with `BUCKET_BOUNDS_MICROS` to label the axis rather than
    /// hard-coding the bounds; the slice is one longer than that constant,
    /// and the extra trailing element is the overflow bucket.
    pub fn buckets(&self) -> &[u64] {
        &self.buckets
    }
}

/// What the actor has done since the database was opened.
#[cfg(feature = "metrics")]
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct MetricsSnapshot {
    /// Commands executed, i.e. the sum of [`KindSnapshot::turns`]. The two agree
    /// by construction rather than by coincidence.
    pub turns: u64,
    /// Loop iterations that took a queue-depth reading. Always at least
    /// `turns + 1` on a live actor, because the reading is taken on the way in
    /// to a `select!` that has not resolved yet. This is the denominator of the
    /// two means below, and it is exposed so the difference is visible rather
    /// than looking like drift.
    pub depth_samples: u64,
    pub high_depth_mean: f64,
    pub high_depth_max: u64,
    pub low_depth_mean: f64,
    pub low_depth_max: u64,
    /// The longest hold since open and what caused it. `None` before the first
    /// turn, and — honestly — also when every turn so far took under a
    /// microsecond, which on this path does not happen.
    pub longest: Option<(CommandKind, Duration)>,
    /// Turns spent on high-priority work while low-priority work was already
    /// queued (0.12.10, W4.4, D-153).
    ///
    /// The actor's `select!` is `biased` and has **no floor**, so this is the
    /// measurement of a bound the design has always had and never observed.
    /// On its own it is not alarming: a busy database *should* prefer
    /// interactive writes, and this counter rising is that working. Read it
    /// beside [`Self::low_starved_run_max`], which is the number with teeth.
    pub low_starved_turns: u64,
    /// The longest unbroken run of the above — i.e. the most turns any single
    /// low-priority command has waited (0.12.10, W4.4, D-153).
    ///
    /// This is the one that answers "can low-priority work be starved". A large
    /// `low_starved_turns` spread over a long session says the tiers are doing
    /// their job; a large *run* says one specific chunk, rebuild or archive sat
    /// behind that many interactive writes in a row.
    ///
    /// # There is deliberately no forced-yield policy, and the reason changed
    /// (0.13.26, W10.4, [D-199])
    ///
    /// It used to be "adding one now would be fixing a bound nobody has
    /// observed being hit". That premise died twice. [D-153] hit the bound
    /// completely on a synthetic burst, and W10.4 then hit it on an ordinary
    /// one: **four closed-loop writers** — each awaiting its own write before
    /// issuing the next, which is what application code does — starve the low
    /// tier for essentially all of their writes
    /// (`examples/fairness_probe.rs`). The run is bounded by how long the
    /// caller keeps offering interactive work, not by concurrency and not by
    /// anything in this crate.
    ///
    /// **What replaced it is the floor's own price.** "After N starved turns,
    /// take one low-priority command" cannot choose *which* command — the low
    /// queue is an mpsc channel and its head is not inspectable — and at least
    /// one low-priority kind is exempt from [`crate::CHUNK_BUDGET`] **by
    /// contract**: an [`crate::Database::archive`] was measured at 3.3 s
    /// unwindowed on an 8,000-key backlog. So the floor would add an unbounded
    /// term to the interactive worst case in order to unblock work that is
    /// declared not to be latency-sensitive, which is the tier split running
    /// backwards.
    ///
    /// **The lever that does work belongs to the caller**: 1 ms of think time
    /// between a writer's writes takes four writers from ~78 to ~2. Which makes
    /// this field the instrument for a decision the caller owns rather than a
    /// defect report about the actor.
    ///
    /// [D-153]: ../docs/architecture/s13-decision-register.md#d-153
    /// [D-199]: ../docs/architecture/s13-decision-register.md#d-199
    pub low_starved_run_max: u64,
    pub kinds: Vec<KindSnapshot>,
}

#[cfg(feature = "metrics")]
impl MetricsSnapshot {
    /// Kinds that broke the budget, worst first. The one-line answer to "is the
    /// 3 ms bound holding?".
    pub fn budget_violations(&self) -> Vec<&KindSnapshot> {
        let mut v: Vec<_> = self.kinds.iter().filter(|k| k.over_budget > 0).collect();
        v.sort_by_key(|k| std::cmp::Reverse(k.over_budget));
        v
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn every_kind_indexes_to_its_own_slot() {
        for (i, &kind) in CommandKind::ALL.iter().enumerate() {
            assert_eq!(kind.index(), i, "{kind} is out of order in ALL");
        }
        assert_eq!(CommandKind::COUNT, CommandKind::ALL.len());
    }

    /// The budget is a bucket boundary, not a value inside one — so "fits in the
    /// budget" is a prefix sum and needs no interpolation.
    #[test]
    fn the_chunk_budget_is_exactly_a_bucket_boundary() {
        let budget = crate::CHUNK_BUDGET.as_micros() as u64;
        assert!(
            BUCKET_BOUNDS_MICROS.contains(&budget),
            "CHUNK_BUDGET is {budget} µs, which is not a bucket bound: \
             {BUCKET_BOUNDS_MICROS:?}"
        );
        assert_eq!(bucket_of(budget), bucket_of(budget - 1));
        assert_eq!(bucket_of(budget + 1), bucket_of(budget) + 1);
    }

    #[test]
    fn the_overflow_bucket_catches_everything_past_the_last_bound() {
        let last = *BUCKET_BOUNDS_MICROS.last().unwrap();
        assert_eq!(bucket_of(last), BUCKET_BOUNDS_MICROS.len() - 1);
        assert_eq!(bucket_of(last + 1), BUCKET_COUNT - 1);
        assert_eq!(bucket_of(u64::MAX), BUCKET_COUNT - 1);
    }

    /// The packing is the reason `longest` is one atomic: duration high, kind
    /// low, so a `fetch_max` on the word compares the duration.
    #[test]
    fn the_packing_leaves_room_for_both_fields() {
        assert!(
            (CommandKind::COUNT as u64) <= 0xFF,
            "the kind index must fit in the low 8 bits"
        );
        // The ceiling must survive being shifted up by the kind's width.
        assert_eq!(MICROS_CEILING.checked_shl(8), Some(MICROS_CEILING << 8));
        assert_eq!((MICROS_CEILING << 8) >> 8, MICROS_CEILING);
    }

    /// The two halves of a shadow rebuild land on opposite sides of the
    /// budget, and a forged hold is the only way to assert it (0.14.16, D-233).
    ///
    /// The integration suite can run a real rebuild and check that the
    /// violation list comes back empty; what it cannot do is make a *fill*
    /// chunk run long on demand. So the canary lives here, where the hold is an
    /// argument: the same over-budget duration recorded against each half must
    /// produce a violation for one and not the other.
    ///
    /// Without this, widening the exemption to cover both halves would leave
    /// every test in the crate green — `a_swap_over_budget_is_not_a_violation`
    /// included, since it asserts a zero that a broader exemption also
    /// produces. This is the assertion that says the zero means *healthy* and
    /// not *unwatched*.
    #[cfg(feature = "metrics")]
    #[test]
    fn a_long_fill_is_a_violation_and_a_long_swap_is_not() {
        let m = ActorMetrics::new();
        let over = crate::CHUNK_BUDGET + Duration::from_millis(44);

        m.record_hold(CommandKind::ShadowRebuild, over);
        m.record_hold(CommandKind::ShadowSwap, over);

        let snap = m.snapshot();
        let of = |kind: CommandKind| {
            snap.kinds
                .iter()
                .find(|k| k.kind == kind)
                .unwrap()
                .over_budget
        };

        assert_eq!(
            of(CommandKind::ShadowRebuild),
            1,
            "a fill chunk ran {over:?} against a {:?} budget and was not \
             counted. The fill half is the canary D-082 refused to exempt and \
             D-233 kept unexempted; if it stops counting, a regression on the \
             one path the chunked rebuild exists to keep short is invisible.",
            crate::CHUNK_BUDGET
        );
        assert_eq!(
            of(CommandKind::ShadowSwap),
            0,
            "the swap was counted as a violation. It exceeds by construction \
             on every healthy database, so counting it makes \
             `budget_violations()` nonzero forever (D-233)."
        );

        // And the magnitude survives the exemption, which is the half of the
        // argument that decided C over B: exempting removes the *occurrence*
        // from the violation list and touches nothing a reader consults to see
        // the hold grow.
        let longest = snap
            .kinds
            .iter()
            .find(|k| k.kind == CommandKind::ShadowSwap)
            .unwrap()
            .longest;
        assert_eq!(
            longest, over,
            "the swap's hold stopped being recorded when it stopped being \
             counted. `over_budget` counts occurrences; the histogram and \
             `longest` are where growth is visible, and an exemption must not \
             reach them."
        );
    }

    #[cfg(feature = "metrics")]
    #[test]
    fn the_longest_hold_names_the_command_that_caused_it() {
        let m = ActorMetrics::new();
        m.record_hold(CommandKind::AssertEdge, Duration::from_micros(500));
        m.record_hold(CommandKind::Archive, Duration::from_millis(40));
        m.record_hold(CommandKind::UpsertConcept, Duration::from_micros(900));

        let snap = m.snapshot();
        assert_eq!(
            snap.longest,
            Some((CommandKind::Archive, Duration::from_millis(40)))
        );
    }

    /// The regression the packing bug produced: a *short* hold of a
    /// later-declared kind must not outrank a long hold of an earlier one.
    ///
    /// The test above does not catch it, because `Archive` happens to be both
    /// the longest hold and a high enum index — which is exactly why the first
    /// version of the packing shipped past it. Here the two orderings disagree.
    #[cfg(feature = "metrics")]
    #[test]
    fn a_later_declared_kind_does_not_outrank_a_longer_hold() {
        let long = CommandKind::AssertEdge; // index 0
        let short = CommandKind::RebuildFts; // last index
        assert!(short.index() > long.index(), "the fixture needs the gap");

        let m = ActorMetrics::new();
        m.record_hold(long, Duration::from_millis(40));
        m.record_hold(short, Duration::from_micros(1));

        assert_eq!(
            m.snapshot().longest,
            Some((long, Duration::from_millis(40))),
            "the max is being taken over the kind index, not the duration"
        );
    }

    /// The three contractual exemptions must not show up as violations, or the
    /// violation count is noise on any database that archives.
    #[cfg(feature = "metrics")]
    #[test]
    fn an_exempt_kind_over_budget_is_not_a_violation() {
        let m = ActorMetrics::new();
        m.record_hold(CommandKind::Archive, Duration::from_millis(40));
        m.record_hold(CommandKind::AssertEdge, Duration::from_millis(40));

        let snap = m.snapshot();
        let violations = snap.budget_violations();
        assert_eq!(violations.len(), 1);
        assert_eq!(violations[0].kind, CommandKind::AssertEdge);
        assert_eq!(violations[0].over_budget, 1);

        // But the hold is still *recorded* — exempt means "not a violation",
        // not "not measured". A 40 ms archive is exactly what T1.1 exists to
        // shrink, and it cannot be shrunk if it is not counted.
        let archive = snap
            .kinds
            .iter()
            .find(|k| k.kind == CommandKind::Archive)
            .unwrap();
        assert_eq!(archive.turns, 1);
        assert_eq!(archive.mean, Duration::from_millis(40));
    }

    #[cfg(feature = "metrics")]
    #[test]
    fn queue_depth_is_a_mean_and_a_high_water_mark() {
        let m = ActorMetrics::new();
        m.record_turn(0, 4);
        m.record_turn(10, 0);

        let snap = m.snapshot();
        // No command ran, so `turns` is 0 while `depth_samples` is 2. The two
        // counters are different facts and this is the case that shows it.
        assert_eq!(snap.turns, 0);
        assert_eq!(snap.depth_samples, 2);
        assert_eq!(snap.high_depth_mean, 5.0);
        assert_eq!(snap.high_depth_max, 10);
        assert_eq!(snap.low_depth_mean, 2.0);
        assert_eq!(snap.low_depth_max, 4);
    }
}