armdb 0.13.0

sharded bitcask key-value storage optimized for NVMe
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
# T4 gate: decoded value cache ceiling

Measured with `cargo bench -p armdb --bench decoded_ceiling` on AMD Ryzen 7 7700
8-Core Processor (16 threads, SMT2), Linux, 2026-08-04 (decode-cross-check
baseline: `cargo bench -p armdb --bench clean_decode`, a deliberately separate
target, 2026-08-05 — see "Decode cross-check"). Pinned to cores 0-7 via
`taskset -c 0-7` on an otherwise idle machine; default criterion settings
(100 samples, 3 s warm-up, 5 s collection per case). Access pattern is
strictly sequential (`i = (i + 1) % ROWS`) — the friendliest case for the CPU
prefetcher; treat every number below as a floor on real-world random-access
cost, not a ceiling.

**Read this before quoting a number from this document — see "Which numbers
may be quoted" at the end.** Between-run spread at the 32 KiB positions in
this benchmark dwarfs the within-run confidence intervals criterion reports —
on the floor's own six samples, ±8.4% between runs against within-run CIs of
±0.2% to ±3.4%, i.e. from a few times to ~40x wider depending on which run you
compare against. The intervals in the raw output below describe sampling noise
*within* one run, not the true uncertainty of these figures. In particular:
the verdict is **Build**, but the tightest cell (`numeric/256 B`) is a bracket
`[18.04%, 50.56%]` whose **pessimistic end is below the 20% threshold** — cite
it as a bracket, never as a single number and never as "both ends clear 20%."

## Fix history

**Round 1** reported "Build" from numbers silently measuring disk I/O, not
cache hits. **Round 2** fixed that, changed the ceiling's floor, then found
and fixed a subtler defect in the fix itself. **Round 3** found the floor
itself was a contaminated sample and replaced the per-shape floor with a
pooled one, added a committed decode-cross-check baseline, and corrected
several prose defects the earlier rounds' fixes introduced. **Round 4** (this
version) changed **no measured number in the Corrected table**: it corrected a
false statement in the quotability section, and moved the decode-cross-check
baseline into its own bench target because round 3's in-process version was
blind, by construction, to the failure it exists to catch. All four rounds are
recorded here because the verdict's supporting numbers changed materially in
each of the first three, even though the bottom-line verdict never did.

### Round 1: block-cache residency and the floor

The block cache only accepts a block read from a **rotated-out (immutable)**
file — `is_full_block = refs.active_read.is_none() && block_offset + 4096 <=
refs.immutable_total` (`armdb/src/shard.rs`). The original benchmark ran on
`Config::balanced()`'s default `max_file_size` (256 MiB) with an 8 MiB
dataset, so no shard's active file ever rotated, `block_cache` stayed empty
for the entire run, and every `block_cache`-tier read (values <= 8 KiB —
`DiskLoc::is_value_cache_routed`'s threshold) fell through to a real `pread`,
unnoticed because nothing in the crate surfaces "this read touched disk".
`value_cache` (> 8 KiB, the 32 KiB cells) inserts unconditionally regardless
of file mutability, so those two cells were never affected — four of six
cells (both shapes at 256 B and 4 KiB) reported inflated timings.

Fix: a small `max_file_size` plus padding rows (under keys never read during
measurement) that rotate every measured key's block into an immutable file;
`check_residency` (`/proc/self/io`'s `syscr:` line, calibrated against its own
overhead) proves cache residency instead of assuming it; the ceiling's floor
moved from `TypedTree::get` (skips mechanics a decoded-cache hit would still
pay) to the **value-cache-hit `VarTree::get` read at 32 KiB** — operation-
for-operation what a decoded-cache hit does. Full mechanism detail is in the
source file's module doc comment (`armdb/benches/decoded_ceiling.rs`).

An early draft of round 1's fix also padded 32 KiB (for index-size
consistency); that padding built a **fresh shaped value per padding row**,
which fragmented the allocator ahead of `var_typed_tree`'s per-iteration
decode allocation — a reproducible ~4x slowdown, caught by noticing the
number moved and reverting. Padding stayed scoped to <= 8 KiB.

### Round 2: the same defect, still live at 4 KiB, then a subtler one

Re-review found `string/4KiB`'s implied decode (`var_typed_tree − var_tree`)
was ~958 ns against an independently measured clean-`rapira::deserialize`
baseline of ~139 ns — 6.9x off — while every other cell was within ~30%. Two
compounding causes, fixed in order:

1. **Per-row shaped padding, asymmetric between arms.** `VarTree`'s padding
   used one hoisted byte buffer; `VarTypedTree`'s padding built a fresh
   `StringShape`/`NumShape` (a ~4 KiB `String`, for the `string`/4KiB cell
   specifically) per padding row via `$make(...)` — the same defect round 1
   had already found and fixed at 32 KiB, still present at 4 KiB because
   round 1 stopped padding 32 KiB rather than fixing the per-row allocation
   itself. Fix: **one** padding buffer of raw bytes per size, built once,
   written verbatim in **both** arms — `VarTypedTree`'s padding writes go
   through `as_inner().put()`, bypassing the codec so no shaped value or
   encode buffer is ever built for a padding row.
2. **Residual defect: padding volume alone.** With per-row allocation fixed,
   `string/4KiB`'s implied decode was *still* ~910-955 ns — the same
   allocator-sensitivity mechanism, this time triggered by the sheer `put()`
   call count/rotation count of the original 4x-safety-factor padding volume
   (4096 rows), not by anything about what's *in* each row. A standalone
   isolation harness (fresh process, one `var`/`var_typed` collection pair,
   no criterion) swept the padding row count and found `decode from plain
   Vec<u8> copy` — a decode call with **no armdb involvement in the timed
   loop at all** — jump from a clean ~140-160 ns to a corrupted ~910-955 ns
   specifically between 3500 and 4096 rows, reproducibly, while every value
   from 0 up to 3500 stayed clean. Fix: the padding safety factor is 2x, not
   4x (2048 rows here) — independently confirmed clean and still resident
   across repeated runs in the isolation harness, with margin below the
   observed threshold.

Applying byte-only padding to all three sizes (including 32 KiB, to remove
the index-size mismatch between the padded cells and the unpadded floor —
**2304** live nodes vs. 256) was tried again in round 2 now that per-row
allocation was fixed, and reverted again: even fully byte-only padding at
32 KiB still regressed `var_typed_tree/32KiB` by 30-320% while `var_tree/32KiB`
(same padding, no decode) moved only 3-11% — the same allocator-sensitivity
signature. Padding stays scoped to <= 8 KiB; the resulting index-size
mismatch at 32 KiB — the **2304-vs-256 live-node gap** quantified at the start
of this paragraph, worth an estimated **5-15 ns upward bias** on the ceiling
computed against an unpadded 32 KiB floor — is disclosed and accepted **here**,
not silently fixed at the cost of reintroducing a multi-microsecond artifact.
Since round 3 the floor is pooled
from the 32 KiB cells for **all six rows**, so this bias applies to every
row's `ceiling`, not only the 32 KiB ones; it is small against every cell's
margin to the 20% line (the tightest, `numeric/256 B`, has 30+ points of
`ceiling %` headroom, and 15 ns of floor moves it by ~13 points at most —
compare the full-floor-range recomputation under "Corrected table").

Round 2 also documented that its padding-safety-factor margin is not a hard
guarantee — see "Cross-run stability" below, folded in from round 2's
original caveat and confirmed still applicable in round 3.

### Round 3: the floor itself was a contaminated sample

Round 2's published `numeric/var_tree/32KiB` read 78.824 ns — a **+39% jump**
from every other measurement of the same code path across rounds 1-2
(`string`: 52.44, 53.92, 52.96 ns; `numeric`: 55.54, 56.69 ns), on a path
this round never touched (32 KiB has been unpadded since round 1). Round 2's
document rationalized the gap ("byte-cache mechanics at 32 KiB carry some
run-to-run variance") instead of checking it, and cited a within-run noise
section whose confidence intervals (±1.7-4.4%) are an order of magnitude too
narrow to explain a 39% between-run move.

`VarTree` is untyped: the value-cache hit path is `shard_for` + `ValueKey`
construction + a hash `get` + two integer compares + a `ByteView` clone
(`armdb/src/var_tree.rs`) — nothing reads the value's bytes, and the two 32
KiB payloads differ by only 4 bytes (32768 vs 32764). The `string` and
`numeric` `var_tree`/32KiB numbers therefore measure **the same operation**
and have no legitimate reason to diverge by 49% with non-overlapping
intervals, which is what round 2's published run showed
(`[52.825, 53.150]` vs `[77.471, 80.203]`).

Re-measured `var_tree/32KiB` for both shapes, 3 times each, via
`cargo bench -p armdb --bench decoded_ceiling --features "typed-tree rapira-codec var-collections" -- "var_tree/32KiB"`
(this filter runs both shapes' `var_tree/32KiB` cells and nothing else is
timed, though the full setup for every cell still executes — see "Cross-run
stability"):

| run | `string/var_tree/32KiB` | `numeric/var_tree/32KiB` |
|---|---:|---:|
| 1 | 53.088 ns | 50.966 ns |
| 2 | 53.701 ns | 54.628 ns |
| 3 | 57.749 ns | 60.247 ns |

`numeric` returned to `string`'s own range on every one of the three
re-measurements — **78.824 ns was a one-off outlier, not a reproducible
shape effect.** Fix: the per-shape floor is retired. Since the code path is
shape-independent *by construction*, not merely by observation (see the
mechanism description above), a per-shape floor was never justified in the
first place — a single **pooled floor** replaces it, computed as the mean of
these six re-measured samples: **55.063 ns**, with an honest range of
**50.966-60.247 ns** across the six samples (this range, not a criterion CI,
is this document's stated uncertainty for the floor — see "Cross-run
stability"). Every `ceiling %` in the Corrected table below is recomputed
against this single floor for both shapes.

Three further corrections this round, all in the sections below rather than
here: the old "32 KiB row's `ceiling` == `decode-only share`" framing was an
algebraic identity (floor = that row's own `var_tree`, true even for an
anomalous sample), not the consistency check it was described as — removed.
The decode cross-check is blind to inflation shared by both arms of its
subtraction, which is exactly what let the contaminated floor through
undetected — documented in "Decode cross-check". And the cross-check's
baseline figures were hardcoded from an ad hoc probe that was never
committed — replaced with a `clean_decode` benchmark group shipped in
`armdb/benches/decoded_ceiling.rs` (round 4 moved that group out into its own
bench target, for the reason below).

### Round 4: the committed tripwire could not fire, and a false quotability claim

Round 4 re-derived nothing. `bench_ceiling` was **not** re-run; every figure in
the Corrected table, the pooled floor, the bracket and every `decode-only
share` are exactly as round 3 published them. Two defects were fixed.

**1. The `clean_decode` tripwire was desensitised by construction.** Round 3
shipped it as a benchmark *group inside* `armdb/benches/decoded_ceiling.rs`, so
it ran in the same process, after `bench_shape!`'s setup for all six cells —
a criterion filter gates only sampling, never that setup (`filter_matches` →
`if do_run { analysis::common(..) }`,
`criterion-0.8.2/src/benchmark_group.rs:303`). The failure mode this tripwire
exists to catch is **process-wide**: round 2 measured a plain-`Vec<u8>` decode
with no armdb in the timed loop — precisely what `clean_decode` measures —
inflating from ~140-160 ns to ~910-955 ns under the corrupting condition (see
"Round 2" above). On a corrupted run the baseline would therefore inflate in
lockstep with the implied decode it checks, the ratio would stay near 1.0x, and
the tripwire would report "passes" on the exact failure round 2 caught at 6.9x.
Worse, round 3 made that blind baseline **authoritative** over the bare-process
probe that is immune to this, on a *reproducibility* argument.

Fix, structural rather than prose: the measurement now lives in its own bench
target, `armdb/benches/clean_decode.rs` (same six shapes, same generators, same
"decode from a pre-serialized buffer, no armdb call in the timed loop"
property), so it runs in a fresh process by construction. **The tripwire
baseline must come from a process that has not run `bench_ceiling`'s setup** —
stated in both bench files, and the blind-spot subsection under "Decode
cross-check" now names this case explicitly. Round 3's in-process figures are
kept below, labelled as history, with what was wrong with them.

Re-running the new target three times also **withdraws round 3's explanation of
the ~30% baseline gap**: round 3 attributed it to shared process history, but a
bare process in this repository reproduces `string/4 KiB` at 151-177 ns, next
to the in-process 179.25 ns and *not* next to the external probe's ~135-139 ns,
while this target's own run-to-run spread reaches ~25% of its mean. The gap is a
build/harness difference plus ordinary between-run spread, not process history.
The structural defect stands regardless: it follows from round 2's measured
6.9x process-wide inflation, not from the size of this particular gap.

**2. A false sentence in "Which numbers may be quoted".** That section said
both ends of `numeric/256 B`'s `[18.04%, 50.56%]` bracket "clear the 20% Build
threshold". 18.04% does not — the whole reason the cell is presented as a
bracket is that its pessimistic end sits **below** the threshold, as this
document says at "`numeric/256 B`: read as a bracket" and in the Verdict
caveat. The sentence appears to have been copied from the `string/256 B`
paragraph, where it is true. It has been corrected, and it sat in the one
section the banner at the top of this document sends a quoting reader to
first — so for the period between round 3 and round 4 this file licensed
exactly the over-claim the bracket exists to prevent. Also corrected in the
same pass: three places called `numeric/256 B` "the widest bracket of the
six", which is true only as a *ratio* (2.80x end-to-end, vs 1.77x for
`string/4 KiB`); in percentage points `string/4 KiB` is wider (36.9 vs 32.5).

### Cross-run stability (applies to round 2's padding margin *and* round 3's floor)

**Neither the 2x padding-safety margin nor the pooled floor's precision is a
hard guarantee.** Every cell of `decoded_ceiling` — including the floor
re-measurement, and including the `clean_decode` group as round 3 shipped it —
runs inside `bench_ceiling`'s process after `bench_ceiling`'s setup for **all
six** `(shape, size)` cells has already executed: criterion's `-- filter`
argument only skips *sampling* for non-matching benchmark IDs, not the plain
Rust setup code that populates and pads every collection, which is
unconditional. So even a "narrow" re-measurement shares full-process
allocation history with every other cell. (Round 4 moved the tripwire out to
`armdb/benches/clean_decode.rs` — a separate binary — precisely so that *one*
measurement escapes this property. Nothing else does, and no criterion filter
can make it.)

During round 2's investigation, running the full 18-cell benchmark
repeatedly at the round-2 padding settings produced 4 clean runs and 1
corrupted run (`string/4KiB` implied decode ~955 ns vs. an independent
baseline of ~139 ns) out of 5 total, with identical code and config — most
likely address-space-layout-driven nondeterminism in glibc's arena
placement, not a deterministic function of any tunable constant. Round 3's
floor re-measurement (three samples per shape, above) is consistent with
this same picture: real sample-to-sample spread on an unpadded, untyped code
path, not error in the measurement method. **Treat every number in this
document as accompanied by this caveat, not just the floor and the padding
margin specifically** — this paragraph is the one place it is stated in
full; the Corrected table and the Verdict each carry a short pointer back
here rather than repeating it.

## Floor

`TypedTree::get` and a hypothetical decoded-cache hit share only the
prologue — `metrics`, `index.collector().enter()`, `SkipList::get`. A decoded
hit living inside the `Var` stack would additionally pay `shard_for(&node.key)`,
`ValueKey` construction, the cache lookup, verify-on-hit, and the handle
clone (`armdb/src/var_tree.rs`, the `value_cache` hit path) — exactly what a
`value_cache` hit already does. `var_tree`/32 KiB **is** that path (32 KiB is
above `DiskLoc::is_value_cache_routed`'s 8192-byte threshold for every offset,
so it always hits `value_cache`, never `block_cache`), and its mechanics are
shape- and size-independent by construction: a `quick_cache`-style hash
lookup + verify + `ByteView` clone that never reads the value's bytes, not
proportional to the value's byte length or shape.

**Floor = 55.063 ns**, pooled across both shapes' three re-measured samples
(see "Round 3" above for the raw values and why a per-shape floor was
retired). Honest range: **50.966-60.247 ns** — this is the between-run
spread, not a criterion confidence interval; see "Cross-run stability."

`TypedTree` stays in the benchmark and the raw-output table below for
reference (it is still a legitimate "index lookup + hand back a pointer"
number, and it never touches storage — residency-checked at 0 syscalls in
every cell), but the verdict does not use it, and it must not be quoted as a
cost either — see "Which numbers may be quoted."

## Decode cross-check

Each cell's **implied decode** (`var_typed_tree − var_tree`, same size) is
compared against an **independently measured clean decode** — `rapira`
decoding the exact same shape, straight from a pre-serialized buffer, with no
armdb call in the timed loop at all.

**Blind spot 1 (numerator), read before trusting a "passes" verdict:** because
implied decode is a same-size *subtraction*, inflation shared by both arms
cancels. This is exactly how the round-2 floor contamination passed the
round-2 cross-check undetected: `numeric/32KiB`'s implied decode (2118.18 ns)
divided by its clean baseline landed at 0.89x — a clean-looking ratio — while
`numeric/var_tree/32KiB` (one of the two inputs to that subtraction) was
itself the 78.824 ns outlier. **"Cross-check passes" means "no decode-side
corruption specific to this cell" — it does not mean "every number that fed
this cell is correct."** The cross-check cannot catch an error that inflates
(or deflates)
both `var_tree` and `var_typed_tree` by roughly the same amount — only a
review of the floor's own stability (as in round 3) catches that.

**Blind spot 2 (denominator), the one round 4 fixed:** *a baseline measured in
the same process as the cells it checks cannot detect process-wide inflation.*
A ratio only fires when the numerator moves and the denominator does not. The
corruption this tripwire exists to catch is process-wide by nature — round 2
measured it directly on a plain-`Vec<u8>` decode with **no armdb call in the
timed loop**, the exact shape of the baseline itself, inflating from ~140-160
ns to ~910-955 ns. A baseline sharing that process inflates with the
numerator, the ratio stays near 1.0x, and the tripwire reports "passes" on the
failure round 2 caught at 6.9x. Round 3 shipped the baseline as a group inside
`decoded_ceiling.rs`, which is exactly that arrangement (a criterion filter
gates sampling, not setup — see "Cross-run stability"), and then made it
authoritative over the bare-process probe that is immune. **The baseline must
be measured in a process that has not run `bench_ceiling`'s setup** — hence
the separate `armdb/benches/clean_decode.rs` target. If a future change folds
that measurement back into `decoded_ceiling.rs`, the tripwire silently stops
being one.

### Baseline of record: `armdb/benches/clean_decode.rs` (separate process)

The tripwire is its own bench target — same six `(shape, size)` shapes as
`bench_ceiling`, same generators, decoded from a plain pre-serialized
`Vec<u8>`, no armdb call in the timed loop, **and no `bench_ceiling` setup in
the process**, which is the property that makes it a tripwire at all (see
"Blind spot 2"). Run it alone, ~1 minute:

```
cargo bench -p armdb --bench clean_decode --features rapira-codec
```

Round 4 ran it three times (`taskset -c 0-7`, same host as every other number
here). One run is not enough: this measurement's own between-run spread
reaches ~25% of the mean (`string/256 B`: 70.83-90.25 ns), so the **mean of
three** is the baseline of record and the range
is stated beside it.

| shape | size | run 1 | run 2 | run 3 | **baseline (mean)** | range |
|---|---|---:|---:|---:|---:|---|
| string | 256 B | 90.250 ns | 72.430 ns | 70.830 ns | **77.84 ns** | 70.83-90.25 ns |
| string | 4 KiB | 176.97 ns | 156.58 ns | 151.32 ns | **161.62 ns** | 151.32-176.97 ns |
| string | 32 KiB | 892.87 ns | 869.90 ns | 849.63 ns | **870.80 ns** | 849.63-892.87 ns |
| numeric | 256 B | 20.707 ns | 23.471 ns | 22.029 ns | **22.07 ns** | 20.71-23.47 ns |
| numeric | 4 KiB | 228.62 ns | 277.90 ns | 226.38 ns | **244.30 ns** | 226.38-277.90 ns |
| numeric | 32 KiB | 1687.0 ns | 1667.4 ns | 1803.4 ns | **1719.27 ns** | 1667.4-1803.4 ns |

#### Two superseded baselines, kept because their history matters

**Round 3's in-process `clean_decode` group** (74.361 / 179.25 / 890.70 /
23.480 / 276.86 / 1880.6 ns for string 256 B/4 KiB/32 KiB, numeric
256 B/4 KiB/32 KiB). It was committed and reproducible, and round 3 made it
**authoritative** on that basis — but it ran inside `decoded_ceiling`'s
process — the one arrangement in which this baseline cannot fire (see "Blind
spot 2"). It is not the baseline of record and must not be quoted as
one; it is retained only so this section's history reads straight.

**External probes** (round-2 reviewer, then an independent repro), bare
process but built outside this repository, so not reproducible from it:
70.8/71.4, 138.7/135.3, 817.4/826.2, 25.3/23.7, 311.5/308.2, 2383.3/2374.1 ns
in the same cell order. Corroboration only.

**Round 3's explanation of the gap between those two is withdrawn.** Round 3
attributed the ~30% spread (e.g. `string/4 KiB`: 179.25 ns in-process vs.
~135-139 ns external) to shared process history. The new bare-process target
refutes that: it reproduces `string/4 KiB` at 151.32-176.97 ns — adjacent to
the in-process 179.25 ns, not to the external ~135-139 ns — and shows ~25%
run-to-run spread of its own on an identical binary. The gap is a
build/harness difference plus ordinary between-run spread, not process
history. **This does not weaken the structural fix**, which rests on round 2's
directly measured 6.9x process-wide inflation of a no-armdb decode loop, not
on the size of this gap.

### This run's cross-check, against the baseline of record

**Tolerance:** implied decode within ~30% of the clean baseline is treated as
consistent with normal byte-fetch/decode overhead variance; a cell more than
~2x off is a **defect in this benchmark, not a finding about armdb**.

**What that tolerance can and cannot resolve.** The baseline's own between-run
spread is ~25% of its mean (see the table above), which nearly consumes the
~30% band. So this cross-check is sound for the failure it exists to catch —
round 2's 6.9x, and anything of that order — but it **cannot** resolve a
hypothetical 1.5x drift. Read any ratio inside the band as "no signal", not as
a measurement: that is where all six cells below sit, and none of them is
evidence of anything beyond the absence of gross corruption. Tightening the
band would take more baseline runs (the spread is between-run, so it averages
down), not a stricter threshold on three.

Implied decode is unchanged from round 3 (`var_typed_tree − var_tree` from the
Corrected table); only the baseline column moved.

**Numerator and denominator here come from different days and different
processes**: the implied decodes are from the `decoded_ceiling` run of
2026-08-04, the baseline from the three `clean_decode` runs of 2026-08-05 (same
host, same `taskset -c 0-7` pinning). That separation is deliberate — measuring
both in one process is exactly the defect round 4 removed (see "Blind spot 2") —
but it means these ratios carry both measurements' between-run spread, which is
why the last column states the ratio across the baseline's full range rather
than a single number.

| shape | size | implied decode | baseline (mean of 3) | ratio | ratio across the baseline range |
|---|---|---:|---:|---:|---|
| string | 256 B | 87.25 ns | 77.84 ns | 1.12x | 0.97x-1.23x |
| string | 4 KiB | 178.03 ns | 161.62 ns | 1.10x | 1.01x-1.18x |
| string | 32 KiB | 961.94 ns | 870.80 ns | 1.10x | 1.08x-1.13x |
| numeric | 256 B | 20.09 ns | 22.07 ns | 0.91x | 0.86x-0.97x |
| numeric | 4 KiB | 258.31 ns | 244.30 ns | 1.06x | 0.93x-1.14x |
| numeric | 32 KiB | 2118.18 ns | 1719.27 ns | 1.23x | 1.17x-1.27x |

Every cell is within 0.91x-1.23x of the mean baseline, and 0.86x-1.27x at the
worst end of the baseline's own range — inside the ~30% tolerance and nowhere
near the ~2x defect threshold. For comparison, the same implied decodes ran
0.79x-1.28x against the external probes (round 2's comparison) and 0.86x-1.17x
against round 3's in-process baseline. Note that round 3's spread was the
*narrowest* of the three, which is not reassurance: a baseline that moves with
the numerator produces tight ratios whether or not the run is corrupted. What
changed in round 4 is not the width of these ratios but that this baseline
**could** have flagged a defect. No cell in this run is flagged as a
decode-cross-check defect. Remember blind spot 1: this says the *decode step*
is not corrupted in any cell, not that every number in this document is
trustworthy on its own — the floor needed a separate check, which round 3 did.

## Raw criterion output

`bench_ceiling`'s 18 cells are **unchanged from round 2** — round 3's finding
was about the floor, not about `var_tree`/`var_typed_tree`/`typed_tree` at
256 B or 4 KiB, and those numbers do not depend on the floor at all, so no
full re-run was needed or performed. Only the floor changed, from a per-shape
value to the pooled value derived above (Round 3, Floor). Round 4 re-ran
nothing here either — it only re-measured the decode-cross-check baseline, in
its new separate target. Reproduced here for completeness:

```
decoded_ceiling/string/var_tree/256B
                        time:   [79.808 ns 79.884 ns 79.951 ns]
decoded_ceiling/string/var_typed_tree/256B
                        time:   [166.80 ns 167.13 ns 167.53 ns]
decoded_ceiling/string/typed_tree/256B
                        time:   [25.393 ns 25.444 ns 25.502 ns]
decoded_ceiling/string/var_tree/4KiB
                        time:   [190.42 ns 191.64 ns 193.07 ns]
decoded_ceiling/string/var_typed_tree/4KiB
                        time:   [369.24 ns 369.67 ns 370.15 ns]
decoded_ceiling/string/typed_tree/4KiB
                        time:   [22.048 ns 22.061 ns 22.076 ns]
decoded_ceiling/string/var_tree/32KiB
                        time:   [52.825 ns 52.958 ns 53.150 ns]
decoded_ceiling/string/var_typed_tree/32KiB
                        time:   [1.0124 µs 1.0149 µs 1.0172 µs]
decoded_ceiling/string/typed_tree/32KiB
                        time:   [33.170 ns 33.303 ns 33.454 ns]
decoded_ceiling/numeric/var_tree/256B
                        time:   [91.069 ns 91.282 ns 91.537 ns]
decoded_ceiling/numeric/var_typed_tree/256B
                        time:   [110.93 ns 111.37 ns 111.73 ns]
decoded_ceiling/numeric/typed_tree/256B
                        time:   [24.715 ns 24.748 ns 24.777 ns]
decoded_ceiling/numeric/var_tree/4KiB
                        time:   [189.69 ns 190.08 ns 190.47 ns]
decoded_ceiling/numeric/var_typed_tree/4KiB
                        time:   [438.80 ns 448.39 ns 459.94 ns]
decoded_ceiling/numeric/typed_tree/4KiB
                        time:   [23.726 ns 23.772 ns 23.829 ns]
decoded_ceiling/numeric/var_tree/32KiB
                        time:   [77.471 ns 78.824 ns 80.203 ns]
decoded_ceiling/numeric/var_typed_tree/32KiB
                        time:   [2.0975 µs 2.1970 µs 2.2905 µs]
decoded_ceiling/numeric/typed_tree/32KiB
                        time:   [24.402 ns 24.686 ns 25.014 ns]
```

`numeric/var_tree/32KiB`'s `[77.471 ns 78.824 ns 80.203 ns]` above is the
round-3 outlier discussed in "Floor" — reproduced here for completeness, but
it is not used as a floor input; the pooled floor comes from the three-times-
each re-measurement below instead.

Residency check: all 18 `var_tree`/`var_typed_tree`/`typed_tree` cells
reported `RESIDENCY[...] ok: 0 read syscalls over 1024 reads` (raw delta
exactly equal to the calibrated self-overhead — 4 in every cell this run, no
unexplained residual). None fell back to a `pread`. Residency is unaffected
by the floor correction — it was never in question for these cells.

Floor re-measurement raw output (round 3, three independent invocations of
`cargo bench -p armdb --bench decoded_ceiling --features "typed-tree
rapira-codec var-collections" -- "var_tree/32KiB"`, full criterion intervals):

```
run 1: decoded_ceiling/string/var_tree/32KiB
                        time:   [52.993 ns 53.088 ns 53.207 ns]
       decoded_ceiling/numeric/var_tree/32KiB
                        time:   [50.813 ns 50.966 ns 51.149 ns]
run 2: decoded_ceiling/string/var_tree/32KiB
                        time:   [53.614 ns 53.701 ns 53.811 ns]
       decoded_ceiling/numeric/var_tree/32KiB
                        time:   [54.455 ns 54.628 ns 54.803 ns]
run 3: decoded_ceiling/string/var_tree/32KiB
                        time:   [57.652 ns 57.749 ns 57.840 ns]
       decoded_ceiling/numeric/var_tree/32KiB
                        time:   [58.433 ns 60.247 ns 62.534 ns]
```

Run 3's `numeric` interval (`[58.433 ns 60.247 ns 62.534 ns]`, ±3.4%) is
itself the widest of the six re-measurement samples — consistent with "Cross-
run stability," not a second anomaly: still in the same 51-60 ns
neighborhood as every other sample, just at the high end of it.

`clean_decode` raw output — round 4, the separate bench target, three
independent invocations of
`taskset -c 0-7 cargo bench -p armdb --bench clean_decode --features rapira-codec`
(this is the baseline of record; see "Decode cross-check"):

```
run 1: clean_decode/string/256B    time: [90.081 ns 90.250 ns 90.410 ns]
       clean_decode/string/4KiB    time: [176.50 ns 176.97 ns 177.53 ns]
       clean_decode/string/32KiB   time: [892.26 ns 892.87 ns 893.59 ns]
       clean_decode/numeric/256B   time: [20.318 ns 20.707 ns 21.122 ns]
       clean_decode/numeric/4KiB   time: [228.39 ns 228.62 ns 228.87 ns]
       clean_decode/numeric/32KiB  time: [1.6832 µs 1.6870 µs 1.6934 µs]
run 2: clean_decode/string/256B    time: [72.355 ns 72.430 ns 72.507 ns]
       clean_decode/string/4KiB    time: [156.34 ns 156.58 ns 156.87 ns]
       clean_decode/string/32KiB   time: [867.76 ns 869.90 ns 872.91 ns]
       clean_decode/numeric/256B   time: [23.432 ns 23.471 ns 23.524 ns]
       clean_decode/numeric/4KiB   time: [266.75 ns 277.90 ns 289.46 ns]
       clean_decode/numeric/32KiB  time: [1.6661 µs 1.6674 µs 1.6689 µs]
run 3: clean_decode/string/256B    time: [70.634 ns 70.830 ns 71.060 ns]
       clean_decode/string/4KiB    time: [151.25 ns 151.32 ns 151.40 ns]
       clean_decode/string/32KiB   time: [846.66 ns 849.63 ns 852.71 ns]
       clean_decode/numeric/256B   time: [21.524 ns 22.029 ns 22.573 ns]
       clean_decode/numeric/4KiB   time: [226.23 ns 226.38 ns 226.55 ns]
       clean_decode/numeric/32KiB  time: [1.7173 µs 1.8034 µs 1.8958 µs]
```

The three runs are the same binary on the same idle host, minutes apart, and
still move by up to ~25% of the mean (`string/256B`: 90.250 → 70.830 ns, a
19.4 ns spread on a 77.8 ns mean) — another
instance of "Cross-run stability," and the reason the baseline of record is a
mean of three with its range stated rather than a single run's number.

Round 3's in-process `clean_decode` output, superseded (kept as history — see
"Two superseded baselines"):

```
clean_decode/string/256B    time: [74.157 ns 74.361 ns 74.598 ns]
clean_decode/string/4KiB    time: [178.85 ns 179.25 ns 179.74 ns]
clean_decode/string/32KiB   time: [888.99 ns 890.70 ns 892.48 ns]
clean_decode/numeric/256B   time: [23.278 ns 23.480 ns 23.634 ns]
clean_decode/numeric/4KiB   time: [272.57 ns 276.86 ns 281.97 ns]
clean_decode/numeric/32KiB  time: [1.8792 µs 1.8806 µs 1.8823 µs]
```

## Corrected table

**Uncertainty note (see "Cross-run stability"): the intervals in the raw
output above are within-run criterion CIs (±1.7-4.4% typically). At the 32
KiB positions the between-run spread is several times to ~40x that (the
floor: ±8.4% between runs, against within-run CIs from ±0.2% to ±3.4%) — do
not read the table below as precise to the last decimal place; read the floor as 55.063 ns with
an observed spread of 50.966-60.247 ns (−4.10 ns / +5.18 ns around the mean,
i.e. 9.28 ns end to end across six samples — a full width, not a ±) and the
32 KiB `var_tree`/`typed_tree` entries the same way.**

| shape | size | `var_tree` | `var_typed_tree` | `typed_tree` (ref only — do not quote as a cost) | floor (pooled) | ceiling | ceiling % | decode-only share |
|---|---|---:|---:|---:|---:|---:|---:|---:|
| string | 256 B | 79.88 ns | 167.13 ns | 25.44 ns | 55.06 ns | 112.07 ns | 67.05% | 52.20% |
| string | 4 KiB | 191.64 ns | 369.67 ns | 22.06 ns | 55.06 ns | 314.61 ns | 85.10% | 48.16% |
| string | 32 KiB | 52.96 ns | 1014.90 ns | 33.30 ns | 55.06 ns | 959.84 ns | 94.57% | 94.78% |
| numeric | 256 B | 91.28 ns | 111.37 ns | 24.75 ns | 55.06 ns | 56.31 ns | 50.56% | 18.04% |
| numeric | 4 KiB | 190.08 ns | 448.39 ns | 23.77 ns | 55.06 ns | 393.33 ns | 87.72% | 57.61% |
| numeric | 32 KiB | 78.82 ns\* | 2197.00 ns | 24.69 ns | 55.06 ns | 2141.94 ns | 97.49% | 96.41%\* |

\* `numeric/32KiB`'s own `var_tree` (78.82 ns) is the identified outlier —
kept in the table because it is what this run actually measured for that
specific row's `var_tree` and `typed_tree`, but it must not be read as this
shape's floor (see "Floor" above) or quoted as a cost. Its `decode-only
share` (96.41%) is a same-size subtraction against its *own* (outlier)
`var_tree`; a non-outlier `var_tree` for this row (e.g. `string/32KiB`'s
52.96 ns, or the pooled floor's 55.06 ns) would push `decode-only share`
slightly higher, since the numerator (`var_typed_tree − var_tree`) grows as
`var_tree` shrinks — treat 96.41% as a lower-bound estimate for this one
figure, not as precise as the other five rows' `decode-only share` values.

`ceiling = var_typed_tree − 55.063` (single pooled floor, both shapes, all
six rows); `ceiling % = ceiling / var_typed_tree`.

`decode-only share = (var_typed_tree − var_tree) / var_typed_tree` — same-size
subtraction, i.e. what fraction of *this size's own* `var_typed_tree` cost
disappears if you remove exactly the decode step, leaving that size's own
`var_tree` byte-fetch mechanics (block-cache extraction for 256 B/4 KiB,
value-cache clone for 32 KiB) untouched. `decode-only share` is
**floor-independent** — it does not use the pooled floor at all, only each
row's own `var_tree` — which is why round 3's floor correction left it
untouched and why it is safe to quote for all six cells (with the
`numeric/32KiB` caveat above).

`ceiling %` and `decode-only share` are **no longer identical at 32 KiB by
construction**, now that the floor is pooled rather than per-row: compare
`string/32KiB`'s 94.57% (`ceiling %`) against 94.78% (`decode-only share`) —
close, because the pooled floor (55.06 ns) happens to sit near
`string/var_tree/32KiB` (52.96 ns), but no longer an algebraic tautology.
(Round 2's document called this identity a "built-in consistency check";
it was not a check — it held for any value including the round-2 outlier,
which is exactly why the outlier went undetected. That claim is withdrawn.)

### `numeric/256 B`: read as a bracket, not a point estimate

`ceiling %` (50.56%) and `decode-only share` (18.04%) land on opposite sides
of the 20% line for this cell — the only one of the six where that happens,
and the widest bracket of the six *in relative terms*: its ends differ by
2.80x, against 1.77x for the next widest, `string/4 KiB`. In percentage
points it is **not** the widest — `string/4 KiB` spans 36.9 points
(`[48.16%, 85.10%]`) against this cell's 32.5 — but `string/4 KiB`'s bracket
sits entirely far above the threshold, so its width costs nothing. The gap
exists because `ceiling %` subtracts the pooled *32 KiB* floor (a cheaper,
different mechanism — value-cache clone, not block-cache extraction) while
`decode-only share` subtracts *this row's own* 256 B `var_tree` (91.28 ns,
already more expensive than the floor because block-cache extraction at
256 B costs more than a value-cache clone). `ceiling %`'s 50.56% therefore
partly credits switching away from block-cache extraction entirely — an
assumption about how a future decoded-value cache would be built, not
something this benchmark measures directly. `decode-only share` makes no
such assumption: only 18.04% of today's cost is decode itself, holding the
byte-fetch mechanics fixed.

Treat `[18.04%, 50.56%]` as an honest bracket: the pessimistic end
(decode-only share) is below the 20% threshold, the optimistic end
(ceiling %) is well above it. **Guidance for small numeric values should be
written from the bracket, not from the optimistic end alone** — see "Which
numbers may be quoted" below.

`string/256 B`'s bracket (`[52.20%, 67.05%]`) does not straddle 20% — both
ends clear it comfortably, no ambiguity there.

**Within-run noise check (see "Cross-run stability" for why this is not the
full uncertainty picture):** the widest within-run interval in this run is
`numeric/var_typed_tree/32KiB` (`[2.0975 µs 2.1970 µs 2.2905 µs]`, ±4.4%).
Recomputing `ceiling %` at both ends (floor held at 55.063 ns) gives
97.37%-97.60% — no movement. `numeric/256B`'s own within-run interval
(`var_typed_tree` `[110.93, 111.73]`) gives `ceiling %` 50.36%-50.72% at
fixed floor — again no movement *within this run*. But per "Cross-run
stability," the floor's honest range (50.966-60.247 ns) is the dominant
source of uncertainty, not this interval: recomputing `numeric/256B`'s
`ceiling %` across the full floor range gives **45.90%-54.24%** — still
entirely on the "Build" side of 20%, but a materially wider band than the
within-run CI alone would suggest.

## Verdict

- **Build**, using `ceiling %` as the rule's metric: it clears 20% in every
  one of the six cells (50.56% to 97.49%, using the pooled floor). No cell
  drops the plain-"Build" case to "Build, small values excluded" or "Do not
  build" under the stated rule, and this holds across the floor's full
  50.966-60.247 ns range (see "Cross-run stability" — do not read this
  verdict as more precise than that range supports).
- **Caveat, not a footnote:** `numeric/256 B` is a genuine bracket
  (`18.04%-50.56%`) — the only cell whose two ends straddle the 20% line, and
  the widest of the six relative to its own size (2.80x end to end; in
  percentage points `string/4 KiB` is wider at 36.9, but harmlessly so — both
  its ends are far above the threshold). The formal verdict is "Build"
  because the rule is defined on `ceiling %`, but anyone writing user-facing
  guidance for small numeric values should present the bracket, not just
  50.56% — round 1's document claimed "there is no size class where the win
  collapses"; that claim is withdrawn. Whether the win "collapses" for small
  numeric values depends on an assumption (how cheaply a real decoded-cache
  hit path could be built) this benchmark cannot verify.
- Three rounds reached the same bottom-line verdict on different grounds:
  round 1 from numbers quietly measuring disk I/O in four of six cells;
  round 2's first pass from numbers with a proven, still-live allocator
  artifact at `string/4KiB`; round 3 from a floor that was itself a
  contaminated sample, rationalized rather than checked. Round 4 re-derived
  nothing and left every figure above untouched — it fixed a false claim in
  the quotability section and made the decode tripwire capable of firing.
  Task 2 should
  proceed; Task 9's documentation should carry the `numeric/256 B` bracket
  forward, not a flat "always build" claim, and should not quote precision
  this benchmark does not have (next section).

### Which numbers may be quoted

- **Safe to quote at face value:** `decode-only share` — 52.20% / 48.16% /
  94.78% / 18.04% / 57.61% / 96.41% for string 256 B/4 KiB/32 KiB, numeric
  256 B/4 KiB/32 KiB respectively. It is floor-independent, so round 3's floor
  correction does not touch any of the six. **Two exceptions to "at face
  value":**
  - `numeric/256 B`'s 18.04% is as soundly measured as the other five, but it
    is *also* the pessimistic end of that cell's bracket — so it may be quoted
    as that cell's `decode-only share`, never alone as that cell's **win**.
    For the win, quote `[18.04%, 50.56%]`; see the `numeric/256 B` bullet
    below, which governs.
  - `numeric/32KiB`'s 96.41% inherits that row's own `var_tree` outlier —
    directionally consistent with the others, not as precisely stated.
- **Quote with the bracket, not as a single number:** `numeric/256 B`'s win —
  state `[18.04%, 50.56%]`, not 50.56% alone.
- **Must never be quoted as a cost:** `floor_numeric` in its round-2 form
  (78.824 ns — a discredited outlier, superseded by the pooled 55.063 ns) and
  `string/typed_tree/32KiB` (33.30 ns — `typed_tree` is a reference-only
  column in this document, never part of the ceiling/floor computation, and
  this particular cell moved +26% between rounds on an untouched code path
  for the same reason the floor did).
- **Do not present criterion's within-run confidence intervals as this
  document's uncertainty.** At the 32 KiB positions the between-run spread
  observed across rounds 1-3 runs from a few times to ~40x the within-run CI —
  on the floor's six samples, ±8.4% between runs against within-run CIs of
  ±0.2% to ±3.4%. Use the ranges stated in "Cross-run stability" and the
  floor's 50.966-60.247 ns instead. Round 4's tripwire target shows the same
  pattern on a binary with no armdb in it at all: ~25% of the mean between
  three runs, against within-run CIs of ±0.05% to ±5.1% (widest:
  `numeric/32KiB` run 3, `[1.7173 1.8034 1.8958]`).
- **The cell with the least margin to the 20% line, quoted as its full
  bracket:** `numeric/256 B`, `[18.04%, 50.56%]`. "Tightest" here means
  closest to flipping the verdict, not narrowest interval — it is in fact the
  widest bracket of the six relative to its own size (2.80x end to end), and
  `string/4 KiB` is wider in percentage points (36.9 vs 32.5) without being
  anywhere near the line. **Its pessimistic end, 18.04%, is _below_ the 20%
  Build threshold**; the formal verdict is "Build" because the rule is defined
  on `ceiling %` (50.56%), not because both ends clear the line. Quote the
  bracket — never either end alone, and never as "both ends clear 20%."
- **The decode-cross-check baseline may only be quoted from a run of the
  separate `clean_decode` bench target** (mean of three: 77.84 / 161.62 /
  870.80 / 22.07 / 244.30 / 1719.27 ns, with the ranges in "Decode
  cross-check"). Round 3's in-process figures and the external probes are
  history and corroboration respectively — neither is the baseline of record.
  A baseline measured inside `decoded_ceiling`'s process is not a tripwire at
  all; see "Blind spot 2".

## Final A/B

> **This section was rewritten after the PR review** (`docs/superpowers/reviews/26-08-05-decoded-value-cache-pr125.md`).
> The original recorded a single run per comparison and derived a realised win
> from a mismatched scenario pair. Both are corrected below. Every figure here
> comes from `rustc 1.97.0`, `x86_64-unknown-linux-gnu`, `cpu_set: 0-7` of 16
> cores, `reps 6`, harness blob `c993143afc13d7eb6c2b0509413eec0a562b3b9f`.

### The realised win, on a matched pair

The realised `get_shared` win is measured against
`read/var_typed_tree/get_direct` — **not** against `read/var_typed_tree/get`.
The latter drives the RPC handler (`db.rpc_handler_for(...).get(&key)`), a
different call path; subtracting it would credit the decoded cache with removing
the handler's dynamic dispatch, key parsing and `ValueBytes` construction, none
of which it touches. `get_direct` shares one setup function with `get_shared`
(`read_var_typed_decoded`) — same config, dataset, seed and warm-up, `get` as
the only difference — so the pair is the same shape by construction.

Both scenarios warm **every** key before the timed loop. Without that pass the
loop takes its first touch of each of the 50 000 keys cold and reports a
75%-hit mixture rather than a resident cache.

Same build, `20360469` → `484d0ef3`, medians of 6 reps (two independent runs):

| scenario | run 1 | run 2 | what it is |
|---|---:|---:|---|
| `read/var_typed_tree/get_direct` | 764.13 | 765.42 | byte path + decode, every call |
| `read/var_typed_tree/get_shared` | 213.71 | 210.77 | warm decoded cache |
| `read/var_typed_tree/get` | 801.18 | 788.62 | RPC handler — *not* the baseline |

**≈72% off a warm point read** (764 → 214 ns), stable across runs. It exceeds
the Task 1 ceiling for a comparable shape because the two measure different
things: Task 1's `ceiling %` isolates the **codec**, while `get_shared` also
skips the byte tier — the value copy and the block/value-cache probe. The
value here is 16 encoded bytes (`VarReadRecord`), the smallest case in the
suite; do not read it as a figure for large values, where the codec share is
higher (Task 1: 94-97% at 32 KiB) but so is the byte-tier cost.

### Byte-path regression gate — inconclusive, and why

The gate command:

```bash
MERGE_BASE=$(git merge-base main HEAD)          # b5be3548
armdb/benches/ab.sh raw "$MERGE_BASE" 484d0ef3 read 6 0-7
```

Run four times, it returns `whole_run_verdict: FAIL` every time, on
`read/var_typed_tree` scans: `range16` +4.07…+4.86%, `page16` +4.32…+6.55%,
`page512` +3.94…+4.67%, `range512` +2.87…+4.00%, `keys512` +1.78…+2.52%,
`get` +1.06…+4.24%.

That verdict does **not** survive its own controls:

| control | result |
|---|---|
| Self-check `484d0ef3` vs `484d0ef3` (identical source, two clones) | **PASS**, every scenario within ±2.13% — so run-to-run error inside one invocation is ≈2% |
| merge-base → `83680aff` (Task 2: the module exists, nothing uses it) | **PASS**, `var_typed_tree` scans −0.76…+0.27% |
| merge-base → `05c451d0` (Task 4: `VarTree::read_decoded` exists, uninstantiated) | **PASS**, −0.08…+0.26% |
| merge-base → `20360469` (Task 7: the `decoded_cache` field on `VarTypedTree` **and** the whole shared API) | **PASS**, −1.81…+0.12% |
| merge-base → `f4e87fd7` (Task 8: the Map API too) | **PASS**, −2.29…+0.49% |
| `20360469` → `484d0ef3` (×2) | **PASS**, −0.44…+2.89% and −1.36…+1.75% |

Every link of the chain passes; only the endpoint fails. 0% + 2% does not make
6.5%, so at least one of the two readings is measurement, not code. Two further
observations point the same way:

- the same revision measures differently across invocations: Task 7's
  `page16` median was 10319 ns/op in one run and 10703 ns/op in another (+3.7%),
  which is larger than several of the deltas the gate calls a regression;
- `typed_tree` — whose source is **byte-identical** on both sides of every one
  of these runs — moved −5.68%, −5.50%, −4.22% and +3.53% on individual
  scenarios across the same runs. A source-identical scenario cannot regress;
  that spread is the harness's cross-build floor, and it brackets the
  `var_typed_tree` deltas.

The most likely mechanism is codegen, not logic: each invocation clones into a
fresh `mktemp` path, the path enters the package id and therefore the symbol
hashes, and the crate grew by ~1 500 lines — enough to move inlining decisions
and code layout. Nothing in the byte read path changed; `git diff` over the
whole branch shows `armdb/src/var_tree.rs` and `armdb/src/var_map.rs` are pure
insertions, with the single deletion anywhere in `armdb/src/` being one `use`
line in a test module.

**So the no-regression claim rests on code identity, not on this A/B.** The A/B
adds a weak second opinion at best. Treat a `FAIL` from this gate on a diff of
this size as a prompt to check the chain, not as a regression report.

> **Harness breakage found while running these controls — unresolved.** Two of
> the controls could not be run at all: `raw 484d0ef3 b5be3548` (the endpoint with
> the sides swapped) and `raw b5be3548 b5be3548` (a self-check on the merge-base)
> both die in the build, 3 attempts out of 3, with `error: failed to run rustc to
> learn about target-specific information` / `character literal may only contain
> one codepoint` — where the source rustc reports compiling is git's own
> `fatal: path 'armdb/src/decoded_cache.rs' does not exist in 'HEAD'`. The same
> revisions in the opposite order build and run fine. Cargo probes the toolchain
> with `rustc - --print=…`, which reads its program from stdin, so that was the
> first suspect; giving both builds `</dev/null` does **not** fix it, so the vector
> is still unknown and the note in `ab.sh` says so. Practical effect: the swapped
> -side control for the byte-path gate above is missing, and the reading of that
> gate stands on the chain of intermediate comparisons instead.