windows-ioring-sys 0.2.0

Memory-safe Rust over the Windows IoRing submission/completion ring, with zero-allocation buffer handoff.
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
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
# Completed checklist: windows-ioring-sys

Append-only. See [CHECKLIST.md](CHECKLIST.md) for pending and in-progress work.

## Moved 2026-08-22 -- M1 through M6: ring lifecycle through consumer documentation

### M1 -- Ring lifecycle and capability negotiation

- [x] **M1.1** -- Create the crate skeleton: `Cargo.toml`, `src/lib.rs`, README, and add it to the
  workspace `members`. `windows-sys` features are `Win32_Foundation`, `Win32_System_IO`, and
  `Win32_Storage_FileSystem` (which is where the IoRing bindings live). Everything is behind
  `cfg(windows)`, as elsewhere in this repository.

- [x] **M1.2** -- A typed owner for `HIORING` whose destructor is `CloseIoRing`, per the repository rule
  that a resource with a specialized destructor gets its own owner rather than a generic handle wrapper.
  Not `Clone`.

- [x] **M1.3** -- Capability query and version negotiation (D-6). `QueryIoRingCapabilities` needs no ring,
  so expose it as a free function for consumers deciding whether to use the crate at all. Ring creation
  negotiates `min(highest we understand, caps.MaxVersion)`, stores it, and exposes it. Surface
  `UM_EMULATION` rather than hiding it: a consumer reaching for this crate to maximize throughput needs to
  know the ring is emulated.

- [x] **M1.4** -- Probe every op once at construction into a cached capability set, plus
  `supports_raw(op_code)` for ops the OS has and this crate has not wrapped (D-7). The public op enum is
  `#[non_exhaustive]`.

- [x] **M1.5** -- Integration test: create and close rings at each version the machine supports, assert
  capability reporting is self-consistent (a ring never claims an op its capability set denies), and assert
  a ring created at a negotiated version reports that version back through `GetIoRingInfo`.

### M2 -- Operation identity, buffer ownership, and rundown

- [x] **M2.1** -- Duplicate `IoBuf`/`IoBufMut` from `windows-overlapped-io-sys` (D-1), with the contract
  extended to cover registration outliving a single operation. Record the extension in DESIGN-NOTES; do
  **not** extract a shared crate yet -- that decision is M6+.

- [x] **M2.2** -- `Token<B>` owning its buffer, with a generation-stamped `usize` as the `UserData` carried
  through the SQE and returned in the CQE (D-4). Validation on completion rejects a token that does not
  match the generation, so a stale token cannot claim a later operation's completion -- the same guarantee
  `OperationId` makes in `windows-overlapped-io-sys`, but cheaper here because `UserData` is ours to choose
  rather than being an address we have to stamp separately.

- [x] **M2.3** -- `Drop` on an uncompleted token forgets its buffer rather than freeing it (D-4). Test that
  the leak happens and that nothing is freed, because a use-after-free here would be silent and remote.

- [x] **M2.4** -- In-flight accounting and rundown: `CloseIoRing` must not run with operations outstanding.
  Mirror the rundown discipline `windows-overlapped-io-sys` already uses, including its bounded, rechecked
  wait rather than an unbounded one.

### M3 -- The submission builder

- [x] **M3.1** -- `Batch` holding `&mut IoRing`, submitting on drop, with `submit()` consuming it and
  returning the submitted count (D-5). Document loudly that the submission queue is ring state, so a
  dropped batch still submits -- the alternative would strand SQEs whose buffers a later unrelated submit
  would hand to the kernel.

- [x] **M3.2** -- Per-op builders reached from the batch, each returning a token from `push()`:
  `read`, `write`, `flush`, `cancel`. Options chain (`offset`, `drain_preceding` for
  `IOSQE_FLAGS_DRAIN_PRECEDING_OPS`) so the common case stays short and the barrier stays discoverable.
  Every method checks its op's capability bit from M1.4.

- [x] **M3.3** -- Submission-queue backpressure: `push()` surfaces `IORING_E_SUBMISSION_QUEUE_FULL`
  (`0x80460002`, observed at exactly entry 64 on a 64-entry queue) as a distinguishable error rather than
  auto-flushing. Auto-flush would silently change submission ordering and timing.

- [x] **M3.4** -- `submit_and_wait(n, timeout)` exposing the fused form, which is the primitive Model B is
  built on (D-3) and not merely a convenience.

- [x] **M3.5** -- The narrow unsafe raw-SQE seam (D-7), documented with the same framing as the `device`
  family's unsafe `ioctl`: it exists so a consumer is not blocked on us wrapping a new op.

- [x] **M3.6** -- Integration tests: a batch of many reads round-trips with every `UserData` preserved and
  every buffer returned; a deliberately overfilled queue reports backpressure at the right entry and stays
  usable afterwards; a dropped batch still submits; a cancel of a target that is not outstanding reports
  `ERROR_NOT_FOUND` through the completion rather than at build time.

- [x] **M3.7** -- *(added during execution)* Completion retrieval: `IoRing::try_pop` popping one
  `Completion` (identity plus result) without blocking, so a caller matches it against a held `Token` via
  `claim_if`. **Re-plan:** M3's original items assumed `Token::claim_if` alone was enough to exercise the
  M3.6 round-trip, but nothing in M1/M2 exposed a way to actually pop a *typed* completion outside of
  `IoRing`'s internal, untyped rundown drain -- M3.6 cannot be tested without this. Added and implemented
  in the same pass as M3.1-M3.6 rather than deferred, per the re-planning discipline.

### M4 -- Model A delivery: completion event and the thread pool

- [x] **M4.1** -- Refuse to construct the event-driven path when `SET_COMPLETION_EVENT` is absent, with
  `io::ErrorKind::Unsupported`, rather than silently degrading into a thread-based loop. Behavior is owned,
  not inherited: a consumer who asked for threadless delivery and got a thread has been told something
  false. The threaded path is a separate, explicit choice (M6+).

- [x] **M4.2** -- Wire `SetIoRingCompletionEvent` to a `ThreadpoolWait` from `windows-threadpool-sys`,
  using an auto-reset `WaitableHandle::event`. The drain discipline is drain-to-`S_FALSE`, re-arm, drain
  again: the spike showed the event is set when completions land and auto-resets on wait, so the worst case
  is a harmless spurious callback, but the ordering is what makes a completion arriving between the last
  pop and the re-arm impossible to lose.

- [x] **M4.3** -- Teardown: quiesce the wait, run down outstanding operations, then close the ring, in that
  order. Interaction with `CleanupGroup` documented, since a consumer will reasonably expect to put this
  in one.

- [x] **M4.4** -- Integration test: submit with `wait_operations = 0` and assert completions are delivered
  on pool threads without the submitting thread ever waiting; assert teardown with operations in flight
  neither hangs nor closes the ring early.

### M5 -- Registration

- [x] **M5.1** -- Registered file handles: a typestate carrying the registered index, so a `read` against
  a registered file cannot be written against an unregistered one by mistake.

- [x] **M5.2** -- Registered buffers, with the ownership rule that registration outlives any single
  operation (M2.1) and the pinning cost documented, because it is the axis that punishes over-sharding
  (D-8).

- [x] **M5.3** -- Integration test: a read addressing both a registered file index and a registered buffer
  index round-trips; a registration outliving many operations stays valid; dropping the registration while
  operations are in flight is refused rather than permitted.

### M6 -- Documentation for consumers, and the guidance that motivated this crate

- [x] **M6.1** -- Render the "Two delivery architectures" material from [DESIGN-NOTES.md]DESIGN-NOTES.md
  as crate-level rustdoc and README content, not only as a maintainer-facing design note. Consumers
  reaching for this crate are trying to maximize I/O throughput; the Model A / Model B trade-off, the
  reason the NUMA node is the wrong partitioning key, and the observation that buffer placement likely
  dominates thread placement are the things they need and cannot easily derive.

- [x] **M6.2** -- A worked example of Model A: threadless delivery through the pool, which is the shape most
  consumers should start with.

- [x] **M6.3** -- Topology guidance as documentation rather than as API (D-8): how to enumerate L3 domains
  with `GetLogicalProcessorInformationEx`, why processor groups are a hard floor, and how to allocate a
  node-local pool with `VirtualAllocExNuma`. Pointers, not a partitioning policy.

## Moved 2026-08-23 -- M7: `ring-copy`, a topology-aligned sample

- [x] **M7.1** -- The `Topology -> Policy -> RingPlan` pipeline, with `Policy` as named code (`ByL3`,
  `ByNode`, `ByPackage`, `ByCore`, `Single`) rather than data. A plan names, per domain, the ring to
  create, the processors to affinitize to, and where its buffer pool should be allocated.

- [x] **M7.2** -- Reject a plan the platform cannot express, rather than emitting an impossible affinity
  mask: a fed-in description may carry one group with more than 64 processors, which is legal in the
  description and unrepresentable on Windows (topology D-10).

- [x] **M7.3** -- The copy engine itself: read and write through per-domain rings, buffers allocated with
  `VirtualAllocExNuma` and registered once per ring.

- [x] **M7.4** -- Switches for topology source (discovered, or a JSON file), policy, and **buffer
  placement** (node-local versus deliberately remote). Buffer placement is the variable most likely to
  show a real effect, because the device DMAs into the registered buffer on every operation, whereas
  callback placement is a one-time cache-warmth question.

- [x] **M7.5** -- Report per-domain throughput, and **say plainly when the host cannot show a
  difference** -- a single-node or virtualized machine will produce noise, and a benchmark that reports
  noise as a result is worse than no benchmark. The machine this was designed on reported zero NUMA nodes.

## Moved 2026-08-28 -- M11: the completion event as a ring primitive

### M11 -- The completion event as a ring primitive (external consumer proposal, 2026-08-28)

Prompted by a consumer proposal and the spike that answered it; the exchange is recorded in
[DESIGN-SESSION-2026-08-28-completion-event-multiplexing.md](design-sessions/DESIGN-SESSION-2026-08-28-completion-event-multiplexing.md),
and the decisions are [D-19](DESIGN-NOTES.md#d-19) through [D-22](DESIGN-NOTES.md#d-22).

M11.1 and M11.2 build the primitive; M11.3 then consolidates `EventDelivery` onto it *and* fixes the
stranded-backlog bug in one change. Those were separate items when the fix looked like it needed to ship
ahead of the API work; they are merged because in practice the two land minutes apart, and splitting them
would mean writing a `SetEvent`-after-arm patch that M11.3 immediately deletes.

- [x] **M11.1** -- Added `IoRing::completion_event(&mut self) -> io::Result<OwnedHandle>`
  ([D-20]DESIGN-NOTES.md#d-20): capability-checks `IORING_FEATURE_SET_COMPLETION_EVENT`, creates and owns
  an auto-reset event, attaches it with `SetIoRingCompletionEvent`, signals it once, and returns a
  duplicate. Idempotent -- a repeat call returns another duplicate of the same event rather than attaching a
  new one, which matters because `SetIoRingCompletionEvent` *replaces* rather than adds, so a second attach
  would silently detach the first subsystem's event. The rustdoc states the
  [D-19]DESIGN-NOTES.md#d-19 contract in full: signalled on empty -> non-empty, drain to empty before
  waiting again, a wake with nothing to pop is normal. Ordering detail worth keeping: the event is stored on
  the ring *before* it is signalled, so no later failure can drop it and leave the ring signalling a closed
  (possibly recycled) handle; and because a manual `Drop` body runs before its fields drop, `CloseIoRing`
  always runs before the event handle closes. Contract tests are M11.2; a throwaway smoke check confirmed
  the setup signal, auto-reset, idempotence, and -- the one that matters for M11.3 -- that attaching to a
  ring with a completion *already* queued does signal.

- [x] **M11.2** -- Contract tests for the M11.1 primitive in
  [tests/completion_event.rs]tests/completion_event.rs, written against the *stated* rules rather than
  against current behaviour: eleven cases covering the setup signal, the backlog case (submit, let the
  completions land, *then* attach, assert the returned handle signals), idempotence, the edge itself
  (empty -> non-empty signals; a batch of eight produces exactly one wakeup; the edge re-arms after each
  drain; a full drain leaves no leftover signal), the duplicate's independence (closing one duplicate,
  and outliving the ring), the capability gate's `Unsupported` branch, and the multiplexed wait.
  Each rule is named in the test that pins it, so a failure is readable without opening
  [DESIGN-NOTES.md]DESIGN-NOTES.md.

  Every case was **verified by sabotage**, which is what makes these contract tests rather than
  behaviour snapshots -- and doing it corrected the suite three times, each correction worth keeping:

  - Suppressing `completion_event`'s setup `SetEvent` fails all eleven, the backlog case on its own
    message. That is the property M11.3 depends on.
  - Making the repeat call attach a *second* event initially failed only **one** test. The other two
    idempotence cases passed **vacuously**: asserting that the handle returned by the *second* call
    signals is satisfied perfectly well by a freshly attached event, so a silently detached first handle
    went unnoticed -- the exact bug those tests exist to exclude. Both were rewritten to assert through
    the *first* handle, and to close the *second* duplicate rather than the first; the sabotage now
    fails all three.
  - The multiplexed test's first shape proved that draining mattered but not that draining on *every*
    pass did. Sabotaging round 1's drain to a single `try_pop` fails at **round 2**, not round 3,
    because round 2's unrelated-wake drain rescues the seven stranded completions -- which is precisely
    why rule 2 says every pass and not merely every ring pass. A round 4 was added that breaks rule 2
    deliberately and asserts the resulting lost wakeup as a timeout, so the deadlock is demonstrated
    rather than described; a control run confirms the same wait returns immediately when the queue was
    drained first, so the assertion measures the edge and not an impatient timeout.

  These are integration tests because generating completions crosses the filesystem boundary, matching
  [tests/event_delivery.rs]tests/event_delivery.rs. The suite runs in about half a second, nearly all
  of it the one deliberately-asserted timeout.

- [x] **M11.3** -- `EventDelivery::new` re-expressed on top of `IoRing::completion_event`, which removed
  the second `SetIoRingCompletionEvent` call site ([D-20]DESIGN-NOTES.md#d-20) and fixed the
  stranded-backlog bug ([D-19]DESIGN-NOTES.md#d-19) in one change. `src/` now has exactly one
  `SetIoRingCompletionEvent` call, in `IoRing::completion_event`.

  The repro landed first and was **watched failing**: a ring handed over with eight completions already
  in its CQ delivered nothing, and
  `completions_queued_before_handover_are_still_delivered` in
  [tests/event_delivery.rs]tests/event_delivery.rs failed on a five-second delivery timeout. After the
  consolidation it passes instantly. The two pre-existing tests in that file both hand over a *fresh*
  ring, which is exactly why the bug survived CI. `completion_event`'s signal-once-on-attach is what
  makes it pass; `ThreadpoolWait` takes the returned duplicate through
  `WaitableHandle::assume_waitable`, and the drain/re-arm/drain callback body needed no change.

  Also removed: `EventDelivery`'s own copy of the capability check and its `Unsupported` error message,
  which duplicated `completion_event`'s word for word. One statement of that rule now, not two.

  Documentation corrected in the same commit, per CONTRACT INTEGRITY's blast-radius rule. The rustdoc
  had asserted the backlog guarantee while the code did not hold it, so it is now stated as a guarantee
  the method *buys* -- naming the edge that would otherwise strand the backlog and the setup signal that
  closes it -- with a note that a caller on an earlier version cannot rely on it. Swept
  `SetIoRingCompletionEvent` / "already queued" / "handed over" across `src/`, `tests/`, `examples/` and
  `*.md`: 6 files updated ([src/event_delivery.rs]src/event_delivery.rs,
  [src/capability.rs]src/capability.rs, [examples/model_a_delivery.rs]examples/model_a_delivery.rs,
  [DESIGN-NOTES.md]DESIGN-NOTES.md's D-20 status marker, its Model A description, and its "this bit us"
  paragraph). Design-session transcripts and [COMPLETED-CHECKLIST.md]COMPLETED-CHECKLIST.md were left
  alone as historical record.

  One newly reachable interaction documented on `EventDelivery::ring`: calling
  `IoRing::completion_event` on a ring the pool is already waiting on now *succeeds*, returning a
  duplicate of the pool's own event, which is two waiters on one ring and a
  [D-21]DESIGN-NOTES.md#d-21 violation. Worth stating because the same call was previously worse and
  equally silent -- it replaced the pool's event and stopped delivery outright.

- [x] **M11.4** -- `windows-threadpool-sys` is now optional behind a default-on `threadpool` feature
  ([D-22]DESIGN-NOTES.md#d-22). Gated with it: the `event_delivery` module and its unit tests, the
  `EventDelivery` re-export, [tests/event_delivery.rs]tests/event_delivery.rs, and
  [examples/model_a_delivery.rs]examples/model_a_delivery.rs -- the example via an explicit
  `[[example]]` with `required-features`, so cargo *skips* it rather than failing to compile it.
  Verified non-vacuous by `cargo tree`: `windows-threadpool-sys` disappears from the graph entirely
  under `--no-default-features`, and the other two examples still auto-discover and build.

  CI gained an `ioring-no-threadpool` job, which is the cost D-22 accepted the gate with: nothing else
  in the workflow builds that configuration, because every `--workspace` step uses the default set and
  `--all-features` turns the gate back on. It runs build, clippy `-D warnings`, test, and doc for
  `--no-default-features`, plus isolated default-feature clippy and test steps (selecting the crate
  alone, so `--workspace` feature unification cannot mask a gap -- the same reason
  `windows-file-watcher` has isolated steps).

  The `cargo doc` step is there because of a defect this item found rather than by symmetry: the
  crate's ungated "Choosing a delivery architecture" prose intra-doc-linked ``[`EventDelivery`]``,
  which resolves under `--all-features` and *dangles* without the feature. The repo-wide `docs` job
  only documents `--all-features`, so it could never have caught it. The link is now plain code naming
  the feature, matching how `windows-overlapped-io-sys` avoids linking its own gated items from ungated
  prose; the failure was confirmed by restoring the link and watching
  `cargo doc --no-default-features` fail with `unresolved link to EventDelivery`.

  Also documented for consumers: a Cargo-features table in [README.md]README.md with the
  `default-features = false` snippet and D-22's actual rationale -- layering, not runtime cost, since
  linking the thread-pool crate creates no threads (the Win32 default pool is process-wide and lazily
  instantiated).

- [x] **M11.5** -- Both facts now stated wherever the wakeup shapes are described, as a blast-radius
  sweep rather than a single edit. Grepped `drain_preceding` / `DRAIN_PRECEDING` / "two delivery" /
  "completion event" / "delivery architecture" across `src/`, `tests/`, `examples/` and `*.md`; 18 files
  matched, 4 changed, and the rest are accounted for below.

  **Fact 1 -- Model B's wakeup source is separable from its identity.** The
  [DESIGN-NOTES.md]DESIGN-NOTES.md "Two delivery architectures" section described Model B as a thread
  parked in `SubmitIoRing` and offered no alternative, which is exactly how the framing came to be read
  as fixing the wakeup mechanism ([D-3]DESIGN-NOTES.md#d-3's amendment note). It gained a subsection
  naming the two wakeup sources -- fused submit-and-wait, and a multiplexed `WaitForMultipleObjects` over
  `IoRing::completion_event` -- as a table, with the point that identity is *who owns, submits and
  drains* and the wakeup is a separate axis. [README.md]README.md gained the same in consumer form.
  [src/lib.rs]src/lib.rs already stated it (M11.1); its Model B paragraph gained a forward pointer so a
  reader who stops there does not leave with the fixed-wakeup reading.

  **Fact 2 -- `drain_preceding`'s barrier stops at the ring's edge.** Stated in
  [DESIGN-NOTES.md]DESIGN-NOTES.md's Category 2 and in [src/lib.rs]src/lib.rs, but **not on
  `PushOptions::drain_preceding` itself** -- the one place a consumer actually meets the flag. That was
  the real gap this sweep found. Its rustdoc now states all three measured properties: ring-wide and
  spanning submissions with no cross-epoch pipelining ([D-24]DESIGN-NOTES.md#d-24), powerless in both
  directions across the ring boundary with `IoRing::completion_event` as what this crate offers instead,
  and that the flag orders but does not flush -- while being what makes a flush cover preceding writes
  at all ([D-23]DESIGN-NOTES.md#d-23). README states the limit too, as the reason the multiplexed
  shape has to exist.

  **Accounted for, unchanged:** `IoRing::completion_event` and the `IoRing` type docs in
  [src/ring.rs]src/ring.rs already state both facts correctly (M11.1);
  [src/event_delivery.rs]src/event_delivery.rs and
  [examples/model_a_delivery.rs]examples/model_a_delivery.rs are Model A only and make no wakeup claim
  beyond it; [src/batch/tests.rs]src/batch/tests.rs and the integration tests are uses of the flag and
  the primitive rather than statements about them (the cross-path limit is not testable in-crate -- it
  is a fact about I/O this crate does not issue); [PLANS.md]PLANS.md is an index summary with no
  wakeup claim; and [COMPLETED-CHECKLIST.md]COMPLETED-CHECKLIST.md, the design-session transcripts and
  the spikes are historical record, deliberately left alone.

- [x] **M11.6** -- [examples/model_b_multiplexed.rs]examples/model_b_multiplexed.rs: a caller-owned ring
  whose `completion_event` is waited on via `WaitForMultipleObjects` alongside a manual-reset shutdown
  latch, draining to empty on every pass, over three waves of reads. It also covers the half the item did
  not name but which a consumer hits immediately -- shutdown while I/O is outstanding -- by quiescing
  through `Batch::submit_and_wait` before the ring closes, which doubles as a demonstration that both
  wakeup sources are Model B (M11.5's fact 1) and that switching between them changes nothing about
  ownership.

  **The central claim was verified by sabotage.** Replacing the drain-to-empty with a single `try_pop`
  reproduces the lost-wakeup deadlock exactly: wave 0 pops one of eight completions, the queue never
  returns to empty, the edge never re-arms, and the wait times out with all remaining work stranded. That
  is [D-19]DESIGN-NOTES.md#d-19 as a running program rather than a paragraph, which is the whole point
  of the item.

  Two comments were **removed after measurement contradicted them**, rather than left as plausible
  prose. The example requests shutdown while the final wave is outstanding, and an earlier draft claimed
  that made the drain-on-shutdown-pass and the quiesce load-bearing. Six runs showed otherwise: the loop
  always exits with zero in flight, because `WaitForMultipleObjects` reports the *lowest* signalled index
  and the ring is index 0, so a set shutdown latch can never starve completions. That is a genuinely
  useful fact for a consumer choosing handle order, so the example now states it and reports what
  actually happened on each run instead of asserting an outcome it does not control.

  The example uses no thread pool, so it is also the first real consumer of the `--no-default-features`
  configuration M11.4 added to CI; verified building under both feature sets.

## Moved 2026-08-28 -- M12: durability exposed, and the flush barrier made explicit

### M12 -- Durability: expose it, and stop defaulting it wrong

The kernel exposes a durability parameter on writes (`FILE_WRITE_FLAGS`) and on flushes
(`FILE_FLUSH_MODE`); this crate hardcodes both, so a consumer sees ordering but no way to express
durability at all. Worse, [D-23](DESIGN-NOTES.md#d-23) measured that an unflagged flush does *not*
cover preceding writes -- which made `Batch::flush(&file, PushOptions::default())`, the obvious
spelling, a silent data-loss bug rather than a missing feature. **M12.1 removed that spelling**; the
remaining items expose the parameters the crate still hardcodes.

Decisions: [D-23](DESIGN-NOTES.md#d-23) through [D-25](DESIGN-NOTES.md#d-25). Measurements are
reproduced by the drain spike recorded in
[DESIGN-SESSION-2026-08-28-external-consumer-correspondence.md](design-sessions/DESIGN-SESSION-2026-08-28-external-consumer-correspondence.md).

M12.1 is first because it is a correctness defect in shipped 0.1.2, not an enhancement.

- [x] **M12.1** -- The barrier decision is now explicit for flushes ([D-23]DESIGN-NOTES.md#d-23,
  [D-25]DESIGN-NOTES.md#d-25). `Batch::flush` and `Batch::flush_raw` take a required
  `FlushCoverage` -- `CoversPrecedingOperations` or `Unordered` -- **in place of** `PushOptions`,
  rather than alongside it. Replacing it rather than adding to it is the point: keeping both would let
  a caller write `FlushCoverage::Unordered` with `PushOptions::new().drain_preceding(true)` and mean
  two contradictory things at once. `PushOptions` carries exactly one decision, and for a flush
  `FlushCoverage` *is* that decision.

  An enum rather than a `bool` because `flush(&file, true)` does not say what the `true` decides, and
  a two-variant type makes the wrong choice unwriteable by accident rather than merely discouraged.
  The `Unordered` variant is documented with the two uses that are legitimate (host sequencing, and a
  flush not being used for durability at all) so it reads as a deliberate choice rather than an
  escape hatch.

  Rustdoc on `flush_raw` now states the measured contract in full: that the ring has no FUA and the
  flush is therefore its only durability primitive, that an unflagged flush was measured completing
  while 17 and then 23 of 32 preceding writes were still outstanding, and that durability is a
  property of an epoch rather than of a write. `PushOptions::drain_preceding` cross-references it so a
  reader arriving from the flag side learns the flush no longer inherits the decision.

  A unit test pins the `FlushCoverage` -> SQE-flag mapping, **verified by sabotage**: making
  `CoversPrecedingOperations` map to `IOSQE_FLAGS_NONE` compiles and passes every other test in the
  crate, and would lose data only on power failure -- exactly the class of defect that needs a
  mechanical check rather than review.

  Swept the statements this changed: [DESIGN-NOTES.md]DESIGN-NOTES.md's epoch construction and
  barrier-cost table now name the API instead of the raw flag, [D-25]DESIGN-NOTES.md#d-25 carries an
  implementation-status marker, and the M12 intro above no longer describes a spelling that exists.
  The two `flush_raw` call sites in [tests/submission_lifecycle.rs]tests/submission_lifecycle.rs use
  `Unordered` with a note saying why (they test backpressure, not durability). The broader durability
  sweep across `lib.rs` and `README.md` stays M12.5, which is where those documents first have to
  discuss durability at all.

  **Breaking change**: both flush entry points changed signature.

- [x] **M12.2** -- [tests/flush_barrier.rs]tests/flush_barrier.rs proves the barrier *behaviour*,
  not the flag. The unit test in [src/batch/tests.rs]src/batch/tests.rs pins the enum-to-SQE-flag
  mapping, which shows the flag is set but not that setting it changes what the kernel does; this
  reproduces the D-23 shape against a real device and asserts the difference. Verified by sabotage --
  mapping `CoversPrecedingOperations` to `IOSQE_FLAGS_NONE` fails it on behaviour, not on a flag
  comparison.

  All three of the spike's failed iterations are encoded as requirements rather than left to be
  rediscovered: `FILE_FLAG_NO_BUFFERING` (buffered writes finish in issue order), a pre-written extent
  (extending writes serialize), and a **size asymmetry** between the two phases (uniform sizes do not
  reorder). The third was not in the item's text and cost a full rewrite to find: a first version used
  32 uniform writes and one flush, and skipped on this machine because it could observe nothing.

  **The measurement disagreed with D-23, and the disagreement is now recorded rather than smoothed
  over.** On this machine *no* preceding write ever completed after an unflagged flush -- 0 of 32,
  against the spike's 17 and 23 -- yet 11 of 32 writes queued *after* the flush completed before it.
  Reordering was plainly happening; it just did not manifest as the flush overtaking the writes ahead
  of it. So the control accepts *either* direction of reordering and skips only when it sees neither;
  requiring D-23's specific observable would have made the test silently vacuous here.
  [D-23]DESIGN-NOTES.md#d-23 now carries the amendment, and "The two measured facts" states the
  consumer-facing consequence: **observing that your flush lands last is not evidence you can omit the
  barrier** -- it is incidental behavior of one device stack, which is exactly what PLATFORM INTEGRITY
  says never to bind to.

  The covering case also asserts [D-24]DESIGN-NOTES.md#d-24's other half (writes queued after a
  drained flush are held until it completes), which is the observable that actually discriminates on
  this hardware. Short-write detection guards the whole thing: an unbuffered write with a misaligned
  offset or length would otherwise make every count meaningless.

- [x] **M12.3** -- `FILE_WRITE_FLAGS` is exposed as `WriteCaching` (`Cached` / `WriteThrough`), a
  required argument on all four write entry points -- `write`, `write_raw`, `write_registered`,
  `write_registered_raw` ([D-25]DESIGN-NOTES.md#d-25). A typed enum rather than a raw flag word, and
  `Cached` is `#[default]` so the previous hardcoded behaviour has a name rather than being the
  absence of one.

  The rustdoc states what write-through **is** -- a first-level cache directive whose value is
  latency shaping, since data already at the device makes a later flush shorter -- and what it is
  **not**: not a durability guarantee and not FUA, because whether it becomes a Force Unit Access bit
  depends on the driver, the volume, and whether the device's write cache is enabled, none of which
  this API can see or promise. A write that completes with `WriteThrough` may still be in a volatile
  device cache. That conflation cost the originating exchange a wrong recommendation, so it is stated
  at the type rather than left in a design note.

  A unit test pins the mapping, including that `Cached` stays the no-flag value: silently enabling
  write-through would change latency behaviour for every existing caller without changing any call
  site.

- [x] **M12.4** -- `FILE_FLUSH_MODE` is exposed as `FlushMode` (`Default`, `Data`, `MinMetadata`,
  `NoSync`) on both flush entry points ([D-25]DESIGN-NOTES.md#d-25), with `Default` as `#[default]`
  -- the mode a durability barrier wants and what the crate hardcoded before.

  `NoSync` carries the loudest documentation in the crate, because it is **the one mode that makes
  nothing durable**: it pushes data out of the system cache and stops, so anything in a volatile
  device cache is lost on power failure exactly as if no flush had been issued. Its completion is not
  a commit point and must never be reported to a caller as one. Stated in passing, as the item asked:
  the existence of a distinct "no sync" mode is itself the evidence that the other three *do* issue
  the sync -- nothing in the Win32 documentation says so directly.

  A unit test pins all four mappings. It matters most for `NoSync`: confusing it with any other value
  would turn a commit point into a no-op that still reports success.

  **Note on sequencing.** These two were implemented back to back and are committed together, citing
  both IDs. They are genuinely independent -- separate kernel parameters on separate entry points --
  and that independence was preserved in how the code was written, so re-slicing the commit would be
  bookkeeping rather than history.

  **Breaking change**: all four write entry points and both flush entry points changed signature.

- [x] **M12.5** -- Durability is now stated wherever it is discussed, as a blast-radius sweep.
  Grepped `flush` / `durab` / `write_through` / `WriteThrough` / `drain_preceding` / `FUA` across
  `src/`, `tests/`, `examples/` and `*.md`.

  **The finding was an absence, not a contradiction.** [src/lib.rs]src/lib.rs and
  [README.md]README.md between them mentioned durability *zero times* -- one incidental hit each, both
  the word "flush" in the list of the kernel's seven ops. A consumer reading either document
  front-to-back would have learned that the ring exists, how to choose a delivery architecture, and
  how to size an execution domain, without ever being told that a flush is the only way to commit
  anything or that the obvious spelling of one commits nothing. Both now carry a `Durability` section
  stating the three facts the item names -- no FUA, the flush is the only durability primitive, a
  flush without the barrier covers nothing -- plus the consequence that ties them together: durability
  is a property of an epoch, never of an individual write. README's third point also carries M12.2's
  measurement, that seeing your flush land last is device-dependent and not evidence the barrier can
  be omitted.

  **Accounted for, already correct:** the flush and write rustdoc state all three facts in full
  (M12.1, M12.3, M12.4), which is where the summaries point; `PushOptions::drain_preceding` states the
  barrier's reach and its relationship to durability (M11.5);
  [DESIGN-NOTES.md]DESIGN-NOTES.md's "Durability on the ring" is the long form all of them defer to.
  [tests/flush_barrier.rs]tests/flush_barrier.rs and [src/batch/tests.rs]src/batch/tests.rs are
  checks of the contract rather than statements of it; the `ring_copy` example's writes are a copy
  pipeline with no durability claim to make, and now name `WriteCaching::Cached` explicitly rather
  than inheriting a hardcoded flag.

## Moved 2026-08-29 -- M13: the epoch-committed log worked example

### M13 -- Worked example: consumer-side durability (an epoch-committed log)

[D-26](DESIGN-NOTES.md#d-26) puts durability *policy* with the consumer and Windows *mechanism*
here, which leaves a gap: without a demonstration, every consumer rediscovers the same composition,
and the three measured contracts ([D-19](DESIGN-NOTES.md#d-19), [D-23](DESIGN-NOTES.md#d-23),
[D-24](DESIGN-NOTES.md#d-24)) are exactly the kind that are learned by deadlock or by data loss.
This milestone closes that gap with a worked example, not a library -- it demonstrates the pattern
without this crate owning the policy.

The example is a miniature write-ahead log: records appended through the ring, made durable by
group commit, with durability reported by epoch. It is deliberately the shape a real consumer
needs, and it exercises `windows-ioring-sys` and `windows-threadpool-sys` together.

**Depends on M11.1** (`IoRing::completion_event`) and **M12.1** (explicit flush barrier). Both have
landed, so this milestone is unblocked.

- [x] **M13.1** -- Scaffolding under [examples/epoch_log/]examples/epoch_log/ --
  [main.rs]examples/epoch_log/main.rs and [contract.rs]examples/epoch_log/contract.rs -- with the
  sample's own durability contract written down first, before any code that implements it.

  The contract is phrased as *this program's specification*, not as a description of what the ring
  happens to do, which is the Design Autonomy rule applied rather than cited: the mechanisms it picks
  (a covering flush, the ring's completion event) are recorded as chosen *because* they satisfy the
  specification, and the file states plainly that a dependency which stops satisfying it is the thing
  that is wrong. The guarantee is the item's sentence -- **a record is durable when the commit of the
  epoch containing it has completed** -- with each of its four load-bearing phrases unpacked, since
  "has completed" doing the work of "was submitted" is exactly how this contract gets misread.

  All three non-guarantees the item names are stated as plainly as the guarantee (no per-record
  durability, no ordering within an epoch, no atomicity past the device's power-fail atomic write
  unit), plus two the writing surfaced: nothing is promised about records after the last committed
  epoch -- present, absent, and torn are all legal outcomes of one crash -- and the whole contract
  rests on the device honoring the flush, which nothing here can verify. Those are recorded as an
  explicit `Assumes` clause rather than left silent.

  The contract is also machine-readable (`CONTRACT: &[Statement]`, grouped by `Clause`) and the
  sample prints it, so a reader who only runs the program still learns what it does and does not
  promise. M13.5's verification pass has something concrete to refer back to.

- [x] **M13.2** -- The append path, in [record.rs]examples/epoch_log/record.rs (the format) and
  [append.rs]examples/epoch_log/append.rs (the arena and the push). A record is a 20-byte header --
  magic, sequence, payload length, checksum -- followed by its payload, with each field justified by
  a clause of the contract rather than by convention: the length because replay has no framing of its
  own, the sequence because the contract guarantees no ordering *within* an epoch so the on-disk
  order is not the logical one, and the checksum because the contract says the tail may be **torn**
  and a reader that cannot tell torn from whole cannot honour that clause. The checksum covers the
  header too, so a torn header carrying a plausible length is caught rather than trusted.

  Appends compose into a registered arena (`SLOTS` slots) and push with `write_registered_raw` over
  exactly the record's bytes, deliberately not the owned-`Vec` form: an externally-managed arena is
  what a real consumer has, and it is what makes this sample worth reading. `PushOptions::new()` and
  `WriteCaching::Cached` are both the *unordered, uncached* choice on purpose -- records stream
  unordered within an epoch exactly as the contract says, and the ordering is bought once by M13.3's
  covering flush. The example runs the arena dry on purpose (24 records through 8 slots), so the
  `WouldBlock`-then-drain path is exercised rather than hypothetical.

  **This item found a real gap in the crate and fixed it at the layer, per the mono-repo policy.**
  `RegisteredBuffers` exposed `get` but no mutable accessor, so a registered arena could only ever
  carry bytes the *kernel* had produced -- there was no way to put a record a caller composed into
  one. `ring_copy` never hit it because it reads into a registered buffer and writes back out of the
  same one. Raised rather than worked around, and the engineer chose per-buffer accounting over a
  narrow `unsafe` seam.

  The subtlety that made it more than an accessor: **`&mut self` is not sufficient for safety here.**
  An in-flight operation holds no borrow -- `write_registered` takes `&RegisteredBuffers` for the
  length of the call and the `Token` keeps only a `RegisteredUse` -- so the borrow checker would
  happily allow mutating a buffer the kernel is reading through. `get_mut` therefore pairs `&mut
  self` with a runtime check, and the outstanding count moved from one per *registration* to one per
  *buffer* so that a busy slot does not block its neighbours, which an arena being refilled needs.
  `WouldBlock` and `InvalidInput` are distinct so "busy" and "no such buffer" cannot be confused.
  Sabotage-verified: reverting `get_mut` to the old per-registration semantics fails the new
  neighbour test with "buffer 1 still has 1 operation(s) outstanding".

  Two tests in [tests/registration.rs]tests/registration.rs cover it -- filling a registered buffer
  and writing it back out, and the refusal/neighbour/out-of-range/release cycle. The example also
  reads its own log back and decodes every record, so the claimed format is checked rather than
  asserted; verifying the *contract* (durability, the torn tail) stays M13.5's job.

- [x] **M13.3** -- Epoch bookkeeping and group commit in [commit.rs]examples/epoch_log/commit.rs.
  Records join whatever epoch is open, closing epoch *N* pushes **one covering flush** whose
  `UserData` is *N*'s identity for the rest of its life, and observing that flush's completion is what
  advances `durable_through`. The item's completion test -- a caller can await "epoch *N* is durable"
  and get a truthful answer -- is `Committer::is_durable`, and the example awaits each epoch before
  moving on.

  **Truthfulness is the whole item, so it is asserted rather than assumed.** The example checks
  `!is_durable(closed)` in the window between pushing a commit and observing it, checks monotonicity
  across every epoch below the watermark, and checks that the still-open epoch never reports durable.
  Sabotage-verified: advancing `durable_through` at push time instead of at completion fails on "a
  pushed commit is not a completed one". A failed commit advances nothing, which is the truthful
  answer for the epoch it was closing.

  Two things the writing forced, both recorded in the module rather than left implicit. The barrier is
  **ring-wide**, so a commit stalls the log -- next-epoch appends queue behind it and hold their arena
  slots -- which is D-24's cost made visible rather than described. And because it is ring-wide, a
  commit of *N* may in fact also cover records already pushed into *N+1*; the committer deliberately
  **reports less than is true**, advancing only to *N*, because reporting less than reality is always
  safe and reporting more never is.

  **The record format gained an `epoch` field**, extending M13.2's header from 20 to 28 bytes. Keeping
  the record-to-epoch mapping only in RAM would have made the contract verifiable in this
  demonstration and unverifiable in the situation it exists for: a replay after a crash runs in a new
  process with no memory of which record joined which epoch. The read-back check now asserts every
  record carries the epoch that was open when it was appended.

  The blocking wait here is the fused submit-and-wait; M13.4 replaces it with the multiplexed one,
  which changes the shape but not the accounting.

- [x] **M13.4** -- The event loop in [event_loop.rs]examples/epoch_log/event_loop.rs: the ring's
  `completion_event` waited on by `WaitForMultipleObjects` alongside a manual-reset shutdown latch,
  with the drain outside the match on which handle woke us and draining *to empty*. Ownership does
  not change -- this thread still owns, submits to, and drains its ring -- so it is Model B with a
  different wakeup source, not Model A. The fused wait M13.3 used is kept beside it as the documented
  contrast rather than deleted, since it is the right choice for a log whose only I/O is ring I/O.

  **The item asked for the drain-to-empty rule to be visually obvious, and sabotage corrected which
  half of it actually matters.** Replacing drain-to-empty with a single `try_pop` deadlocks the
  example outright: the queue stops returning to empty, the edge never re-arms, and the next wait
  blocks until its 30-second timeout with the log's work stranded. That half has teeth and is now
  demonstrated.

  Moving the drain *inside* the ring's arm, by contrast, **does not break a conformant loop** -- the
  sabotage passed. On a loop that already obeys rule 1 the shutdown pass has nothing to pop, because
  any completion that arrived signalled the event and was drained on the ring's own wake. An earlier
  draft claimed the unconditional placement was "the bug this file exists to demonstrate the absence
  of"; that claim was false and is removed. The placement stays because rule 1 says every pass and it
  costs nothing, and the file now says plainly that this example cannot make it fail. This is the
  the same overclaim M11.6 caught, found the same way.

- [x] **M13.5** -- The replay-and-verify pass in [replay.rs]examples/epoch_log/replay.rs, which is
  what turns the sample from a demonstration into evidence. It is handed the watermark the log
  reported and holds it to exactly the contract's asymmetry: every record in an epoch at or below the
  watermark **must** be present, in sequence, with a matching payload and a validating checksum,
  while records above it may be present, absent, or torn and are counted rather than judged. Refusing
  to tolerate a torn tail would be its own bug -- the contract promises the tail is unreliable, so a
  reader that treated it as corruption would reject a healthy log.

  The example now appends `TAIL_RECORDS` into an epoch it **deliberately never commits**, because a
  replay pass that never sees an uncommitted tail has not been asked the interesting question.

  **The verifier is run three ways, and the third is the point.** Against the log as written (24
  durable verified, 3 tail tolerated); against a copy cut mid-record to simulate a crash (24 durable
  still verified, tail truncated, *no* violation reported); and -- the negative control -- against a
  copy with one byte corrupted **inside** the durable region, which must be reported. A verifier that
  cannot fail proves nothing, so the sample proves this one can: it reports
  `MissingDurableRecord { reason: ChecksumMismatch }` and the run states out loud that the checker
  which passed the first two cases was shown to be able to fail.

## Moved 2026-08-29 -- M14: crossing the ring boundary, and paying for the barrier

### M14 -- Worked example: crossing the ring boundary, and paying for the barrier

Second half of the example. M13 stays inside the ring; this milestone covers the two things that
forced the original consumer conversation -- operations the ring cannot express, and the cost of
[D-24](DESIGN-NOTES.md#d-24)'s full-barrier stall.

**Depends on M13.**

- [x] **M14.1** -- Order a non-ring operation against ring epochs: an `FSCTL`-class operation issued
  through [`windows-overlapped-io-sys`]../windows-overlapped-io-sys, sequenced at an epoch
  boundary, with its completion waited on in the *same* multiplexed wait as the ring's. This is the
  case `drain_preceding` cannot express at all ([D-24]DESIGN-NOTES.md#d-24 orders SQEs against
  SQEs), and the reason `completion_event` exists. Add the sibling crate as a dev-dependency.
  **Done:** `examples/epoch_log/reclaim.rs` reclaims a retired segment with `FSCTL_SET_ZERO_DATA`
  on a worker thread (that backend completes synchronously, which the event loop must not do), and
  `EventLoop` now waits on three handles. Two measured facts, both sabotage-checked: the reclaim
  running *alongside* appends does **not** make the third handle load-bearing (ring traffic wakes
  the loop; removing the handle costs nothing), whereas the idle-path reclaim after the ring
  quiesces does -- removing it turns a 78 ms run into a 30 s `WAIT_MS` block. The ordering itself
  is enforced by the log, not the ring, and asserting it before each request makes that checkable.

- [x] **M14.2** -- A control-plane and background path on `windows-threadpool-sys`: checkpointing or
  reclamation driven from the pool while the pinned log thread keeps the data path. Demonstrates the
  hybrid the design notes recommend -- Model B on the hot path, Model A for everything else -- in one
  program, which nothing in the crate currently shows. **Must also add an explicit `[[example]]`
  entry for `epoch_log` with `required-features = ["threadpool"]`** (found while doing M13.1): the
  sample uses no thread pool through M13, so it builds under `--no-default-features` today, and the
  moment this item introduces `EventDelivery` it stops -- which the `ioring-no-threadpool` CI job
  from M11.4 will catch as a build failure rather than a warning.
  **Done:** `examples/epoch_log/checkpoint.rs` runs the checkpoint on a *second* ring handed to
  `EventDelivery`; the pool thread that sees the covering flush complete is what authorises the
  reclaim, so the chain crosses log thread -> pool thread -> reclaim worker -> log thread with the
  log thread blocking for none of it. Two rings rather than one is forced by
  [D-21]DESIGN-NOTES.md#d-21, not chosen. The `[[example]]` entry was added and its necessity
  verified: removing it makes `cargo check --all-targets --no-default-features` fail with
  `unresolved import windows_ioring_sys::EventDelivery`, which is exactly what the M11.4 job runs.

- [x] **M14.3** -- Implement all three epoch-commit strategies from "Durability on the ring" behind
  one interface, selectable at run time: covering flush (ring stalls), host sequencing (a userspace
  round trip per epoch), and alternating rings (neither, at the cost of doubled registration). The
  point is that [D-24]DESIGN-NOTES.md#d-24 makes this a real fork with no free answer, and a
  reader needs to see all three to choose.
  **Done:** `examples/epoch_log/strategy.rs`. All three run the same workload and are checked two
  ways: each log replays clean, and all three must be **byte-identical** to each other. Both checks
  were sabotage-verified (a shifted offset trips replay; a dropped epoch trips byte-identity). The
  limit is stated in the code rather than papered over -- neither check can observe whether the
  ordering held on the *device*, which is only visible across a power cut, so the strategies are
  argued from [D-23]DESIGN-NOTES.md#d-23/[D-24]DESIGN-NOTES.md#d-24 rather than from a clean run.

- [x] **M14.4** -- Measure the three strategies on the running machine and print the comparison:
  throughput, commit latency distribution, and ring idle time during the barrier. The example should
  *demonstrate* the trade-off rather than assert it, and the numbers are machine-specific enough that
  quoting ours would be misleading.
  **Done:** throughput, commit-latency quantiles, and append stall are measured per strategy and
  printed with an explicit "these describe THIS machine" note. The finding is that on this machine
  the three are **indistinguishable** -- the cross-strategy spread (1.08x-1.72x) is the same size as
  one strategy's run-to-run spread (1.42x) -- because every strategy pays one device flush per epoch
  at hundreds of microseconds while their actual differences land in the tens. The program computes
  and states that from its own data rather than hard-coding a ranking. Measurement also **found two
  harness bugs** that no other check caught: a single deferred-commit slot shared by two lanes (so
  half the commits were never awaited and `durable_through` was a claim), and pending commits keyed
  by `UserData` in one map across two rings whose sequences collide. Both are recorded in
  `strategy.rs` because both are easy to repeat.

- [x] **M14.5** -- Document the example: a module-level walkthrough, a pointer from `README.md` and
  from the "Durability on the ring" section of [DESIGN-NOTES.md]DESIGN-NOTES.md, and an explicit
  statement that it is a demonstration of a pattern rather than a supported API -- so that nobody
  vendors it and then expects this crate to maintain its policy choices.
  **Done:** `main.rs` opens with the not-API statement and its reasoning (D-8/D-26), a module table
  giving each file's job *and the contract behind it*, what one run does, and two things the sample
  cannot show. `README.md` points at the example from the Durability section and gains an "the
  examples are demonstrations, not API" subsection under Cargo features.
  [DESIGN-NOTES.md]DESIGN-NOTES.md gains a subsection at the end of "Durability on the ring" that
  also promotes M14.4's two findings out of the sample, since both are about the design rather than
  about the demonstration.

## Moved 2026-08-30 -- M8 through M10: handle lifetime, cross-ring identity, and the contract audit

### M8 -- `FileRef::Raw(HANDLE)` lifetime safety (PR #20 review finding)

A caller can close or reuse the raw `HANDLE` passed to `Batch::read`/`write`/`flush`/`cancel`/
`register_files` before the kernel finishes with it: unlike a buffer (owned by a `Token` until claimed),
`FileRef::Raw` carries no lifetime and nothing in this crate borrow-checks a handle across the async gap
between push and completion.

- [x] **M8.1** -- Ownership model decided: `Batch::read`/`write`/`flush`/`cancel`/`register_files` become
  `unsafe fn` when addressing a raw `HANDLE` (their existing `SAFETY` comments already state the
  caller-keeps-it-alive obligation informally; this makes it a real, compiler-checked boundary), paired
  with a safe `SharedFile` wrapper (`Arc<OwnedHandle>`) for the common case: each push clones the `Arc` into
  the same `Token` that already tracks the operation's buffer (or, for `flush`/`cancel`, a standalone
  `Token<Arc<OwnedHandle>>`), so the underlying handle survives until every operation referencing it is
  claimed or leaked, regardless of what the caller does with its own `SharedFile` clone. Rejected:
  borrowing `FileRef::Raw` for the `Batch`'s own lifetime (does not solve the completion-outlives-the-push-
  call problem) and forcing every raw handle through an owning wrapper the way
  `windows-overlapped-io-sys`'s endpoints do (defeats `FileRef::Raw`'s zero-setup reason to exist).
  `FileRef::Registered` needs none of this and stays unaffected.

- [x] **M8.2** -- Added `SharedFile` (`pub struct SharedFile(Arc<OwnedHandle>)`) with a constructor from
  `OwnedHandle`, `Clone`, and a raw-handle accessor for building an `IORING_HANDLE_REF`.

- [x] **M8.3** -- Marked `Batch::read`/`write`/`flush`/`cancel`/`register_files`'s raw-`HANDLE`-taking
  forms `unsafe fn` with a `# Safety` section stating the real obligation (valid handle, correct access
  rights, remains valid until the pushed operation's completion is observed or the ring runs down).
  **Scope correction found during execution:** `read_registered`/`write_registered` take the identical
  `impl Into<FileRef>` shape for their own `file` argument and carry the same hazard, so they were marked
  `unsafe fn` too, with no separate checklist item -- the original enumeration omitted them by oversight,
  not by decision. Committed as `feat(ioring)!`.

- [x] **M8.4** -- Added safe overloads `read_shared`/`write_shared`/`flush_shared`/`cancel_shared` (plus
  `read_registered_shared`/`write_registered_shared`, for the same reason as M8.3's correction) taking
  `&SharedFile`, cloning the `Arc` into the same `Token` (a tuple with the buffer for `read`/`write`-shaped
  pushes, or `Token<SharedFile>` alone for `flush`/`cancel`, which have no buffer of their own) so a caller
  never needs `unsafe` for the common case. `register_files` gets no `_shared` counterpart: a registration's
  handles must stay valid for the ring's remaining life, a lifetime no single push's `Token` can express.

- [x] **M8.5** -- Integration test
  (`dropping_the_callers_own_sharedfile_clone_does_not_close_a_still_outstanding_handle`,
  `tests/submission_lifecycle.rs`): drop the caller's own `SharedFile` clone (its only external reference)
  while a push against it is still outstanding; the read still completes correctly against a live handle,
  proving the `Arc` clone inside the token -- not the caller's copy -- is what kept it open.

- [x] **M8.6** -- Renamed the API so the safe path gets the plain names: the raw/`unsafe` entry points
  became `read_raw`/`write_raw`/`flush_raw`/`cancel_raw`/`read_registered_raw`/`write_registered_raw`, and
  the safe `SharedFile`-taking overloads (previously `*_shared`) took over the plain names
  `read`/`write`/`flush`/`cancel`/`read_registered`/`write_registered` -- since the safe path is the one
  this crate wants to steer callers toward. `register_files` keeps its plain name unsafe, since it has no
  safe counterpart to make way for.

### M9 -- Cross-ring identity and registration-drop safety (PR #20 review findings)

`UserData`, `RegisteredFile`, and `RegisteredBuffers` indices are each meaningful only against the
specific ring that minted them, but nothing enforced that when more than one `IoRing` exists in the same
process.

- [x] **M9.1** -- Added `RingId` (`ring.rs`): a monotonic, process-lifetime-unique `AtomicU64` counter,
  never the ring's own `HANDLE` (which Windows can reuse for the next object after a ring closes). Every
  `IoRing` gets one at construction; every popped `Completion` now carries the id of the ring that
  produced it (D-17).

- [x] **M9.2** -- `Token::claim_if` now requires both the `UserData` identity and the `RingId` to match,
  closing the gap where two different rings' own zero-based `UserData` counters could coincide. Added
  `claim_if_rejects_a_matching_user_data_from_a_different_ring` (`src/token/tests.rs`).

- [x] **M9.3** -- `RegisteredFile`/`RegisteredFiles`/`PendingFileRegistration` and
  `RegisteredBuffers`/`PendingBufferRegistration` now carry the minting ring's `RingId`; `handle_ref`
  (fallible now) and a new `Batch::check_registration_ring` reject a `FileRef::Registered`/
  `RegisteredBuffers` argument from a different ring with `io::ErrorKind::InvalidInput`, checked before any
  `Build*` call runs. Added `a_registered_file_from_a_different_ring_is_rejected` and
  `a_registered_buffers_from_a_different_ring_is_rejected` (`tests/registration.rs`).

- [x] **M9.4** -- `PendingBufferRegistration` now leaks its buffers (via `ManuallyDrop` plus a
  deliberately empty `Drop`) if dropped without a matching `claim_if`, mirroring `Token`/
  `RegisteredBuffers` (D-18) -- previously it dropped `Vec<B>` normally, freeing memory the kernel might
  still reference from an already-queued `BuildIoRingRegisterBuffers` call. Added
  `dropping_an_unclaimed_pending_buffer_registration_leaks_rather_than_frees` (`tests/registration.rs`).

- [x] **M9.5** -- `Batch::do_submit` now marks itself attempted (`self.submitted = true`) *before*
  propagating `SubmitIoRing`'s `HRESULT`, not after: `submit_and_wait` takes `self` by value, so a failed
  call still runs `Drop` once its caller's `self` goes out of scope, and the old ordering left `Drop` free
  to silently retry an already-attempted submit -- which could succeed on the retry without the original
  caller's `Err` ever saying so.

- [x] **M9.6** -- Output-abstraction cleanup in the two examples the review flagged
  (`examples/model_a_delivery.rs`, `examples/l3_domains.rs`): routed through a single writer seam per the
  repository's architectural pre-step, matching `src/bin/run_scenario.rs` (in `windows-file-watcher`) and
  `examples/ring_copy/main.rs`'s existing pattern. `PLANS.md`'s bare `COMPLETED-PLANS.md`/
  `COMPLETED-CHECKLIST.md` references were also made clickable relative links.


### M10 -- Finish the contract audit against the ten specification-gap categories

[DESIGN-NOTES.md](DESIGN-NOTES.md) -> "Specifying this contract" audits this crate against
[the ten categories](../../DESIGN-NOTES.md#specifying-a-delivery-contract). The first pass reached four of
them: D-17's `RingId` (categories 4/5, handled correctly), `Completion::synthetic`'s test-only gate
(category 10, handled correctly), D-14's registration-index continuity (category 4, recorded as an explicitly
unverified assumption), and the previously-unstated completion-ordering rule. Categories 1, 2, 3, 6, 8, and 9
were **not examined**, recorded as "not looked at" rather than "does not apply".

M10.1-M10.3 closed that out, so all ten are now examined. The audit also turned up two API gaps rather than
mere statement gaps; they are M10.4 and M10.5 below rather than notes in the design record, per "design notes
are not a work queue".

- [x] **M10.1** -- Audited category 3 (state-dependent legality unenumerated) against capability negotiation
  ([D-28]DESIGN-NOTES.md#d-28): [DESIGN-NOTES.md]DESIGN-NOTES.md -> "Category 3" now tabulates which
  `Batch` methods each probed op gates, and states the four rules the prose left silent -- `supports` answers
  for the kernel's op table rather than this crate's push surface (`Op::Nop` gates no `Batch` method at all,
  which is exactly the op M6+.3's shutdown case reaches for); `supports_raw` accepts named codes too and
  differs from `supports` in caching rather than truth, while never widening what `Batch` can push; every
  legality check runs before `reserve_user_data`, so a rejected push strands no identity; and the registration
  one-shot is enforced against the registered count, making the real rule "at most one registration that
  assigned an index" and making a *failed* registration unretryable on that ring. Corrected the `supports`,
  `supports_raw`, `Op`, `register_files`, and `register_buffers` rustdoc that stated these wrongly or not at
  all, and added an integration test binding the zero-length-registration rule.

- [x] **M10.2** -- Audited the remaining categories (1, 2, 6, 8, 9) against the ring/token/registration
  surface; [DESIGN-NOTES.md]DESIGN-NOTES.md carries one section each. Category 1: file addressing and
  buffer addressing are genuinely orthogonal (all four combinations reachable), but file addressing and
  *safety* co-vary and in the wrong direction -- `FileRef::Registered` is reachable only through an
  `unsafe fn` whose safety obligation is vacuous for that very input
  ([D-29]DESIGN-NOTES.md#d-29, fixed in M10.4). Category 2: **every successfully queued SQE produces
  exactly one completion**, unconditionally -- the rule `run_down`'s termination silently depended on --
  plus the two "may" readings that were too weak (`submit*` returns entries submitted, not completed; a
  cancel is a request that yields a second completion rather than replacing its target's). Category 6: a
  completion matching no live `Token` is normal, via four distinct routes. Category 8: this crate joins
  nothing, deliberately, and now says so. Category 9: `io::Error::kind()` is lossy exactly where the
  `HRESULT` is not, discriminating this crate's own rejections and never the kernel's
  ([D-30]DESIGN-NOTES.md#d-30, predicate queued as M10.5). Category 7 falls out of 2 and 6 jointly, so
  all ten categories are now examined.

- [x] **M10.3** -- Resolved D-14's unverified registration-index continuity assumption, by **dissolution
  rather than measurement** ([D-31]DESIGN-NOTES.md#d-31). D-14 justified the eager advance on the grounds
  that erring early "can only ever waste indices, never collide two registrations onto the same index" -- and
  that collision needs a *second* registration, which the PR #20 review response forbade the day after D-14
  was written. So `base_index` is always zero, no later base index is ever derived from the count, and the
  kernel's claim timing has no observable consequence; measuring it would settle a fact with nothing
  downstream of it. Took the checklist's second branch for the residue that *is* observable: the public
  counts now state on themselves that they report a **reserved** count, not a confirmed one -- already
  advanced before any completion is popped, and still advanced after a registration whose completion failed
  (which is why that registration cannot be retried, [D-28]DESIGN-NOTES.md#d-28). Added a test binding
  reserved-at-queue-time, gave D-14 the required adjacent status marker, and corrected the audit section's
  category-4 paragraph, which still described the assumption as live.
  **Merge note (2026-08-29):** `main` had meanwhile recorded that the measurement *does* exist -- the
  2026-08-27 session established that a second `BuildIoRingRegisterFileHandles` replaces the whole table and
  re-bases indices at zero, that a table holds at least 65536 handles, and that an index is resolved at
  submission -- and scheduled the follow-up workspace-side as [M19.1]../../CHECKLIST.md, with the
  relaxation of the one-registration-per-ring rule as M19.2. See
  [DESIGN-SESSION-2026-08-27-pseudo-async-namespace-operations.md]../../design-sessions/DESIGN-SESSION-2026-08-27-pseudo-async-namespace-operations.md.
  That does not reopen this item, but it does bound the dissolution: **the dissolution's premise is that a
  second registration is forbidden**, so if M19.2 relaxes that rule, D-14's collision concern becomes live
  again and [D-31]DESIGN-NOTES.md#d-31 must be revisited rather than assumed to still hold. Recorded here
  because nothing else would carry that dependency across the two checklists.

- [x] **M10.4** -- Gave `FileRef::Registered` safe entry points ([D-29]DESIGN-NOTES.md#d-29), via a sealed
  `FileTarget` trait with an associated `Guard` type ([D-33]DESIGN-NOTES.md#d-33) rather than a parallel
  family of `*_registered_file` methods. `read`, `write`, `flush`, `cancel`, `read_registered`, and
  `write_registered` are now generic over it, so a `RegisteredFile` pushes without `unsafe` and the
  file-registered x buffer-registered combination is expressible for the first time. `Guard` carries the
  one real difference between the two targets -- what the `Token` must hold until the completion is
  observed: an `Arc` clone for `SharedFile`, nothing needing to be kept alive for `RegisteredFile` (which
  hands its `Copy` index back for symmetry). **Non-breaking**: `read(&SharedFile, ..)` still resolves to
  `Token<(B, SharedFile)>` and every existing call site compiled untouched; generic params are ordered
  `<B, F>` so an existing `read::<Vec<u8>>` turbofish still resolves. Sealing is load-bearing rather than
  tidiness -- an outside impl could return `FileRef::Raw(arbitrary)` with a guard keeping nothing alive,
  reintroducing what D-16 closed. Three tests: a safe registered-file read, the fully-registered
  composition, and a cross-ring rejection confirming D-17's check still holds on the safe path.

- [x] **M10.5** -- Added named predicates for the ring conditions a consumer must branch on
  ([D-30]DESIGN-NOTES.md#d-30, resolved as [D-34]DESIGN-NOTES.md#d-34), in three parts. A
  `#[non_exhaustive]` `RingCondition` enum covering **every** `IORING_E_*` this crate names -- not only the
  actionable ones, since narrowing it would be the "narrow the platform to serve the visible goal" failure
  PLATFORM INTEGRITY forbids. Predicates (`is_submission_queue_full`, `is_completion_queue_too_full`,
  `is_submit_in_progress`) for just the runtime-actionable conditions, because a predicate asserts a branch
  exists; the rest stay reachable via `condition()`. And a sealed `IoRingErrorExt` putting those answers on
  `io::Error` itself, which is what actually removes the hand-rolled
  `get_ref().downcast_ref::<IoRingError>()` from call sites -- the crate's own integration tests had two
  such helpers, and deleting them was the new API's first use. `IoRingError::name` is now **derived** from
  `condition()` rather than matching the `HRESULT` a second time (CONTRACT INTEGRITY: prefer a derived fact
  to a restated one) -- previously a new condition could be added to one `match` and silently missed by the
  other. Did **not** map `IORING_E_*` onto `io::ErrorKind`; D-30's refusal stands. Bound to a *real*
  kernel-reported queue-full in the existing backpressure integration test, not only to synthetic errors.

- [x] **M10.6** -- Fixed a live use-after-free in `Batch::register_buffers`
  ([D-32]DESIGN-NOTES.md#d-32). `BuildIoRingRegisterBuffers` reads its `IORING_BUFFER_INFO` array when
  the registration op *runs*, not when the `Build*` call returns; the crate built that array in a local
  `Vec` and dropped it before `SubmitIoRing`, so the kernel read freed heap and the registration completed
  with `ERROR_NOACCESS`. Since `register_buffers` is a **safe** `pub fn`, safe code could make the kernel
  dereference a dangling pointer -- a soundness hole in shipped 0.1.2, not a test defect. A spike crossed
  array-lifetime against buffer alignment and disproved alignment in both directions; it also established
  that the array may be released once submit returns, and that `BuildIoRingRegisterFileHandles` genuinely
  *does* read synchronously -- the asymmetry the rustdoc had wrongly generalized across, and the reason the
  file-registration tests always passed. The array is now owned by the `IoRing` rather than the `Batch`,
  because a failed submit leaves the SQE queued as ring state ([D-5]DESIGN-NOTES.md#d-5) and a later
  unrelated submit can be what runs it. Added a regression test that churns the heap between the push and
  the submit, verified by sabotage; corrected both false "read synchronously" claims; moved the entry from
  [UNRESOLVED-TEST-FAILURES.md]UNRESOLVED-TEST-FAILURES.md to
  [RESOLVED-TEST-FAILURES.md]RESOLVED-TEST-FAILURES.md.

## Moved 2026-08-30 -- M15 through M18: the testing-strategy response to eight defects

### M15 -- Deterministic memory instrumentation (release-gating)

Eight defects came out of the 0.1.x line and the M11-M14 branch:
[#47](https://github.com/MikeGrier/windows-threadpool-sys/issues/47),
[#48](https://github.com/MikeGrier/windows-threadpool-sys/issues/48), `get_mut` returning `&mut B`
([D-35](DESIGN-NOTES.md#d-35)), `Appender::claim` leaking an arena slot, the checkpoint path authorising a
reclaim after a failed write, a shared deferred-commit slot in the strategy harness, `get` racing the kernel
([D-36](DESIGN-NOTES.md#d-36)), and a `debug_assert` that could never fire. Sorting them by *what would have
caught them* is what M15-M18 are built from, because they need different tools and one population needs a tool
that does not exist:

- **A -- preconditions never varied.** Every `tests/event_delivery.rs` case handed over a *fresh* ring, so
  "completion queue non-empty at handover" was never a test input. That is
  [#47]https://github.com/MikeGrier/windows-threadpool-sys/issues/47. M17.
- **B -- failure paths never taken.** No test ever ran `completion.result()` returning `Err`, which is the
  checkpoint defect, and is why [#48]https://github.com/MikeGrier/windows-threadpool-sys/issues/48 surfaced
  as a *lucky* `ERROR_NOACCESS` rather than as corruption. M15 and M16.
- **C -- permissions, not behaviour.** `&mut Vec<u8>` *permits* `reserve`/`resize`/reassign; no execution ever
  performs it. That is [D-35]DESIGN-NOTES.md#d-35 and [D-36]DESIGN-NOTES.md#d-36, the two most severe
  findings, and **no runtime technique reaches this population at all.** M18.

**Deliberately rejected: a mock `IoRing`.** Both shipped defects were the kernel behaving differently from
this crate's assumptions -- the completion event is edge-triggered ([D-19](DESIGN-NOTES.md#d-19)), and
`BuildIoRingRegisterBuffers` reads its array at submit rather than at build ([D-32](DESIGN-NOTES.md#d-32)). A
mock encodes the assumption, so one written before those discoveries would have passed both bugs green -- it
would not merely have failed to find them, it would have manufactured evidence they were absent. A model
belongs here as an **oracle over observed sequences** (M16), never as a substitute for the kernel.

**Deliberately rejected: Application Verifier / PageHeap**, after measuring it rather than assuming either way.
See [D-37](DESIGN-NOTES.md#d-37); the short version is that it works but is keyed by *image file name*, and
cargo rehashes test binaries on every meaningful rebuild.

**M15 and M16 gate the 0.2.0 release; M17 and M18 do not.** 0.2.0 carries two security fixes and should not
wait on the state-space work.

- [x] **M15.1** -- Add a guard-page global allocator for test builds: `VirtualAlloc` each allocation with a
  trailing `PAGE_NOACCESS` guard page, right-aligned so its end abuts the guard, and on free flip the block to
  `PAGE_NOACCESS` and **never reuse the address**. That combination makes both an overrun and a
  use-after-free a deterministic `0xC0000005` instead of a silent stale read.
  Use `MEM_DECOMMIT` rather than plain `VirtualProtect` on free: never reusing an address is the point, but
  never releasing the *commit* would grow a long suite without bound.
  A working probe is at `.scratch/guardalloc/` -- measured clean/UAF/overrun as `0`/`0xC0000005`/`0xC0000005`,
  against a system-allocator baseline that silently read a freed byte and exited `0`.
  **Sabotage-verify against the real defect:** revert [D-32]DESIGN-NOTES.md#d-32's fix locally and confirm
  the allocator turns it from a lucky `ERROR_NOACCESS` into a deterministic failure. An instrument never seen
  to fire is indistinguishable from one that cannot.
  **Done:** new `windows-guard-alloc` crate (`publish = false`), installed by `tests/registration.rs`. The
  calibration was run and is the point of the item: reverting [D-32]DESIGN-NOTES.md#d-32 makes
  `a_buffer_registration_survives_heap_churn_between_the_push_and_the_submit` -- the regression test that fix
  added -- die with `STATUS_ACCESS_VIOLATION` instead of passing. Five subprocess tests in `tests/faults.rs`
  prove the allocator fires at all (an access violation cannot be caught in-process), and a
  `the_guard_allocator_is_installed_for_this_test_binary` assertion catches the silent failure mode where the
  `#[global_allocator]` attribute is missing and everything passes uninstrumented.

- [x] **M15.2** -- Fill every allocation and every freed block with a **tracked** poison pattern, derived from
  a per-run seed plus a per-allocation ordinal so the bytes identify *which* allocation they came from rather
  than merely being "not real data". A fixed constant like `0xDD` is worth much less: it collides with real
  payloads, and it cannot answer "where did this come from".
  The tracking is also what keeps this compatible with this component's reproducibility rule: **log the seed at
  test start and accept it from the environment**, so a failure is replayable exactly rather than being a
  one-off. Record that in [DESIGN-NOTES.md]DESIGN-NOTES.md as the terms under which non-fixed test data is
  permitted here.
  Keep the pattern cheap to write (a repeating 8-byte word carrying the ordinal, not a per-byte hash) since it
  runs on every allocation.
  **Done:** `windows-guard-alloc`'s `poison` module. The pattern is `splitmix64(seed ^ ordinal)` repeated
  across the allocation, and the mixing function is a **bijection with a computable inverse**, so
  `GuardAlloc::poison_check` recovers *which* allocation a region belongs to from its own leading bytes --
  no snapshot needed, which is what M15.3 will build on. Seeding terms recorded as
  [D-39]DESIGN-NOTES.md#d-39 and verified end to end: two unpinned runs differ, two pinned runs reproduce
  byte-for-byte, and both decimal and `0x` hex parse.
  **Two findings while implementing.** (1) *Poisoning freed blocks is dead code here* -- `dealloc` decommits,
  so those bytes are unreadable and poison written there could never be observed by anything. It would cost a
  memset per free for a guarantee strictly weaker than the one already in force, so it is deliberately not
  done, and `dealloc` says why. The item text above is left as originally written rather than quietly edited,
  since the planning error is the more useful record. (2) The first draft **hardcoded two multiplicative
  inverses and both were wrong**; `the_multiplicative_inverses_are_actually_inverses` caught it. They are now
  *derived* by a `const fn` Newton iteration, which makes that class of error unrepresentable rather than
  merely tested for.

- [x] **M15.3** -- Verify the poison at the points where the *kernel* is the suspect, which is the gap guard
  pages structurally cannot cover: a guard page only catches access to memory that should not be touched at
  all, and says nothing about the kernel writing into a live, valid allocation. Both checks below are
  invariants this crate asserts in prose today and verifies nowhere:
  - **After a registered *write* completes** (`KernelAccess::ReadsBuffer`): the slot must be **byte-identical**
    to what was submitted. The kernel is only permitted to read it.
  - **After a registered *read* completes** (`KernelAccess::WritesBuffer`): every byte *outside*
    `[span.offset, span.offset + information)` must still be poison. This binds the kernel to the
    `RegisteredSpan` we declared, and would equally catch our own `checked_span` or offset arithmetic being
    wrong.
  Sabotage-verify each by deliberately submitting a span narrower than the write, and by mutating a slot
  mid-flight.
  **Done:** `tests/kernel_span.rs`, five tests. Both named checks, plus two the item did not anticipate: a
  **short read** must leave the unfilled remainder of its span poison (the span is a permission, not a
  promise, so `information` is the only thing that says where real data stops), and a read into one slot must
  leave its **neighbours** untouched -- which is what per-slot ordinals buy over a single constant, since a
  write landing in the wrong slot is visible as such rather than merely as "something changed".
  All four assertions were sabotage-verified individually and each named the right failure: a byte before the
  span, a byte past its end, a modified write source, and a disturbed neighbour. Every message carries the
  seed, so a failure is replayable per [D-39]DESIGN-NOTES.md#d-39.
  **Note the checks cut both ways**, which is worth more than the kernel-conformance reading: the offset the
  kernel receives is computed by *this* crate in `checked_span`, so an off-by-one there fails these tests
  exactly as a kernel bug would -- and is otherwise as invisible as one.

- [x] **M15.4** -- Verify poison at quiescence too: at teardown every registered buffer's never-written
  regions still hold their expected pattern, and no buffer that was only ever a write source has changed.
  This is the whole-arena form of M15.3 and is what catches a stray write attributed to no particular
  operation.
  **Done:** `windows-guard-alloc`'s `witness` module plus
  `a_mixed_workload_leaves_every_unaccounted_byte_poisoned`. A `Witness` starts owning a region with nothing
  permitted; each completion permits exactly the bytes it was entitled to change -- the **transferred** count,
  never the requested one -- and `verify` walks the *gaps* between merged permissions at teardown. Slot 3 in
  the test is a write source only, so nothing is ever permitted for it and it must be byte-identical.
  Both directions were sabotage-verified: a byte in the gap between slot 0's two disjoint reads, and a byte in
  the write-source slot, each reported with the slot, the offset, expected-versus-found, the seed, and how
  many bytes were legitimately written (`0 byte(s)` for the write source). The 12 `witness` unit tests weight
  the **false-positive** direction deliberately -- abutting ranges, overlapping ranges, out-of-order
  permissions -- because a witness that accuses legitimate writes trains a reader to ignore it, which is the
  same failure as missing one.

### M16 -- Make the contract executable, and take the failure paths (release-gating)

Population B, and the invariants this crate already *states* but nothing checks.

**Depends on M15** only for convenience -- the oracle is independent of the allocator, but the two are most
useful together.

- [x] **M16.1** -- Add `src/contract.rs` with a public `RingContract`: the executable form of
  [the category-2 rule]DESIGN-NOTES.md#one-sqe-one-completion -- "every SQE that successfully queues
  produces exactly one completion" -- every token claimed-or-deliberately-leaked, and every per-buffer
  outstanding count back to zero at quiescence.
  **Correction (found while implementing):** this item originally cited D-29/D-30 for that rule. Both are
  about something else -- D-29 is `FileRef::Registered`'s safety and D-30 is `io::Error::kind`'s asymmetry --
  and the rule actually lives in the M10.2 audit's category-2 prose, which had no anchor to cite. It has one
  now. The wrong citation had already reached [PLANS.md]PLANS.md and
  [DESIGN-NOTES.md]DESIGN-NOTES.md itself; all three are corrected. Fed by the caller (`observe_push` / `observe_completion` / `check_quiescent`) rather than
  wired into `Batch`, so a consumer can validate its own harness against the same definition.
  It lives in **this** crate, not in the test harness: the layer that owns the invariant owns the oracle, and a
  harness-side copy is a second implementation of the rule rather than a check of it. Follow
  [`ContractChecker`]../windows-file-watcher/src/contract.rs's shape.
  State plainly what it does **not** check -- it cannot observe device ordering or anything absent from the
  completion stream -- since over-constraining is the same defect as under-specifying.
  **Done:** `src/contract.rs`, 16 unit tests. Five violations: unexpected completion, duplicate completion,
  outstanding at teardown, leaked token, buffer still in use. A completion provisionally marks its operation
  **leaked**, corrected by `observe_claim` -- so forgetting to claim is the default failure rather than the
  silent success, which is the shape of the `Appender::claim` defect. A leak is excused only when *stated*
  via `observe_deliberate_leak`, because the difference between a stated and an unstated leak is the whole
  point. Violations sort before they are reported, since `HashMap` order is unspecified and an oracle whose
  output reorders between runs cannot be diffed. Half the tests are the **does-not-fire** direction, listed
  in the module doc: completion order (the ring promises none between independent operations, and
  [D-24]DESIGN-NOTES.md#d-24's barrier constrains execution rather than pop order), operation success,
  anything about the device.

- [x] **M16.2** -- Bind the existing integration tests to `RingContract` and assert quiescence at teardown.
  This is the item that pays for M16.1: the `Appender::claim` slot leak and the strategy harness's shared
  deferred slot were both conservation failures, found by review and by measurement respectively, and both
  would have fallen out of a quiescence assertion automatically.
  Sabotage-verify by reintroducing one of them and confirming the oracle reports it.
  **Done:** bound `tests/submission_lifecycle.rs` (two tests) and `tests/kernel_span.rs`'s mixed workload,
  which also reports per-buffer counts. **The example's `Appender` now owns a `RingContract`**, because the
  claim that the `Appender::claim` leak "would have fallen out automatically" had to be *proved* rather than
  asserted -- and nothing outside the example could prove it. Calibrated against the real defect: taking the
  pre-fix early-return path once makes the run die with `the token for user_data 0x1 was dropped unclaimed,
  leaking whatever it held for the life of the process`.
  **Binding real tests exposed a gap in M16.1**: `flush_raw` and `cancel_raw` return a bare `user_data` with
  no token, so the oracle demanded a claim that cannot be made -- a false violation the caller has no way to
  satisfy, which is the worst kind. Added `observe_tokenless_push` and a `PushedTokenless` state, with four
  more tests. The backpressure test also exercises the rule's other half: the *rejected* push is deliberately
  never observed, since a synchronously-failing `Build*` releases its reservation and produces no completion.
  Noted for M16.3: reverting the fix's *ordering* alone does not reproduce the leak, because the early return
  only triggers on a **failed** write -- which is precisely the population-B path that has no coverage yet.

- [x] **M16.3** -- Add a fault-injection seam at the completion boundary, so a test can make `result()` return
  a chosen error for a chosen operation. `Completion::synthetic` already exists under `#[cfg(test)]` and is
  half of this. Keep it test-gated for the same reason `synthetic` is: `Token::claim_if`'s safety argument
  depends on every `Completion` tracing back to a real popped `IORING_CQE`.
  **Done:** `Completion::with_injected_failure` plus an `InjectedFailure` enum (`Ring` / `Win32` / `Hresult`),
  behind a new default-off `fault-injection` feature.
  **The design turns on a distinction the item's framing missed.** `synthetic` *fabricates* a completion, and
  the reason it must stay `#[cfg(test)] pub(crate)` is not merely convention: a fabricated completion can name
  an operation still **in flight**, so claiming a token against one hands a buffer back while the kernel is
  writing through it -- the exact use-after-free this crate exists to prevent. Building the seam that way to
  test safety would be self-defeating. So this seam **transforms a completion the ring genuinely popped**
  instead: same `UserData`, same ring identity, same finished operation, and only `result()` changes.
  `claim_if`'s argument is therefore untouched, which is what makes the feature safe to expose at all.
  **Found while writing the tests:** `Hresult(0)` would have injected *success*, silently falsifying the
  "failure only, never success" guarantee and letting a test conceal the very defect it was written to find.
  Now enforced by a panic rather than asserted in prose, with a `should_panic` test on it.
  Also deleted a test that could not verify its own name -- `information` is private and unreachable once
  `result()` is `Err`, so "reports no bytes transferred" had no assertion to write. The zeroing stays (it
  would be wrong to model a state the kernel never produces) but the vacuous coverage does not.
  `tests/fault_injection.rs` proves the seam is reachable *and* gated from outside the crate; CI's existing
  `--workspace --all-features` job runs it, verified rather than assumed.

- [x] **M16.4** -- Use M16.3 to cover the failure paths that have no coverage at all: a failed read, write and
  flush completion claimed normally, a failed registration, and `EventDelivery` delivering a failed completion.
  Assert the *documented* degradation each time rather than merely that nothing panics -- the checkpoint defect
  was precisely a failure that was noticed, recorded, and then not acted on.
  **Done:** `tests/failure_paths.rs`, six tests, all five named cases plus the loop-closer M16.2 left open --
  `claiming_before_checking_the_result_is_what_stops_a_failure_from_leaking` runs *both* orderings against
  `RingContract` on the same injected failure, so the `Appender::claim` defect is a reported violation rather
  than an argument. `EventDelivery` uses a **genuine** `ERROR_NOT_FOUND` from cancelling a non-outstanding
  target rather than an injected failure, per M16.3's own guidance.
  Two sabotage-verified: making a failed registration leak instead of drop, and making `EventDelivery` swallow
  failed completions. Both fired with the right message.
  **Corrected M16.3 while doing this.** That item's soundness argument covered `Token::claim_if` only, and
  `PendingBufferRegistration::claim_if` is materially different: it treats a failed completion as proof the
  kernel did *not* retain the addresses, so it **drops the buffers**. Injecting a failure there frees memory
  the kernel genuinely holds registered. Inert only while nothing uses it -- the test takes the precaution
  explicitly and `with_injected_failure` now documents the limitation, which it did not before.
  **Found a race in the test, not the crate:** the `EventDelivery` callback published the error code *after*
  the counter the waiting thread spins on, so the waiter read a stale zero. Fixed by publishing the data
  before the flag -- the same ordering rule the epoch log's reclaim worker already follows.

### M17 -- Feed the detectors M15 and M16 built

Population A. [#47](https://github.com/MikeGrier/windows-threadpool-sys/issues/47) is one point in a space
nobody was sampling: `{fresh, dirty}` handover state crossed with operation kind, buffer kind, claim/drop and
drain timing. Enumerating that by hand is how it was missed the first time.

**What M15 and M16 changed about this milestone's purpose.** Neither found a single new defect in shipping
code: every finding was either historical ([D-32](DESIGN-NOTES.md#d-32), `Appender::claim`) or a defect in the
new instrumentation itself. That is not evidence the crate is clean. `RingContract`, the guard pages, the
poison, `Witness` and the fault seam are all **passive** -- they fire only on paths the existing hand-written
tests already walk, and #47 lived on a path no test walked. M17 is therefore not a fourth technique standing
beside M15 and M16. It is the **input generator for detectors that are already built and currently under-fed**,
which is also why its cost is lower than when it was first planned: the oracle, the memory checking and the
failure paths all exist now.

**Depends on M16.1** (the oracle is the property these tests assert).

**CONVENTION GATE -- CLEARED.** Randomized property testing is **permitted**, decided by the engineer and
recorded as [D-41](DESIGN-NOTES.md#d-41), so M17.3 is unblocked. Two conditions bind it: seed whatever can be
seeded (so a run replays from one number, and varying that number is how permutations are generated), and
where non-determinism is inherent and outside our control -- multiprocessor scheduling, kernel completion
order -- randomness is fair game. The corollary that keeps the second from becoming a loophole: inherent
non-determinism excuses *irreproducibility*, never *unverifiability*, so a test that cannot be replayed must
instead prove it reached the state it claims to exercise.

- [x] **M17.1** -- Close the two open cells of the handover precondition. A coverage inventory taken after M16
  found this axis is mostly covered already, so this is no longer a matrix to build: *fresh* and
  *queued-non-empty* are covered by [tests/completion_event.rs]tests/completion_event.rs,
  *drained-then-resubmitted* by its `the_edge_re_arms_after_every_drain_to_empty`, and #47's own repro by
  [tests/event_delivery.rs]tests/event_delivery.rs. What remains is (a) attaching while operations are
  **in flight but no completion has landed**, for both `IoRing::completion_event` and `EventDelivery::new`,
  and (b) drain-then-resubmit for `EventDelivery`. Gated on nothing; done first so #47's own axis is banked
  before any convention decision.
  **Done:** [tests/handover.rs]tests/handover.rs, four tests, each bound to `RingContract` so a lost,
  duplicated or unclaimed completion is a contract violation rather than an inferred count.
  **The planned approach for cell (a) was wrong, and its sabotage is what proved it.** The item called for
  racing the attach against in-flight operations. That state does not exist for *buffered* reads: they
  complete **synchronously inside** `submit_and_wait`, measured at 80 of 80 attempts across five shapes up to
  512 reads of 64 KiB, always leaving a full queue at attach time and never a partial split. The first draft
  swept a delay across the supposed window and passed; sabotaging [D-20]DESIGN-NOTES.md#d-20's setup signal
  failed it on *attempt 0*, revealing the sweep sat entirely on the already-queued side and the test was a
  slower restatement of `attaching_to_a_ring_whose_queue_is_already_non_empty_still_signals`. Recorded as
  [D-40]DESIGN-NOTES.md#d-40. Cell (a) is covered two ways instead. **Deterministically:** one attach
  serving a backlog queued *before* it **and** a wave submitted *after* it into the still-non-empty queue,
  which is where a seam between the setup signal and the edge would strand work exactly as #47 did.
  **And genuinely in flight:** unbuffered (`FILE_FLAG_NO_BUFFERING`) reads *are* asynchronous, so the state
  is reachable after all -- reusing the aligned-buffer shape [tests/flush_barrier.rs]tests/flush_barrier.rs
  already had, extended to `IoBufMut`. That test **checks its own precondition**, counting what was already
  queued at the instant of attach and failing if no attempt caught a read in flight, so it cannot decay into
  the already-queued case on faster hardware; dropping only the `NO_BUFFERING` flag makes it fail with
  exactly that message.
  **Calibrated, and the two mechanisms are separable:** suppressing the setup signal fails only the two
  mixed-queue tests; suppressing `EventDelivery`'s `activation.rearm` fails only the re-arm test. Disjoint
  failure sets, so neither test is passing on the other's behalf.
  **Cell (b) was genuinely open, shown by mutation rather than asserted:** with `activation.rearm` removed,
  **all 107 pre-existing tests still pass** -- the entire suite was blind to `EventDelivery` silently
  ceasing delivery after its first drain -- and only the new test catches it. The three targets that did not
  get to run reference no `EventDelivery`, so they could not have caught it either.
  Whether other test files should also bind to `RingContract` is left to **M18.3**'s `cargo-mutants` triage
  to answer with evidence, rather than retrofitted blind now.

- [x] **M17.2** -- Decide and record whether randomized property testing is permitted here, as a
  [DESIGN-NOTES.md]DESIGN-NOTES.md decision. [D-39]DESIGN-NOTES.md#d-39 already fixed the terms under which
  non-fixed test data is allowed in this component -- **seeded, announced, pinnable** -- and M15.2 proved them
  end to end, so this is now the narrow question "does D-39 extend from poison to `proptest`?" rather than an
  open-ended policy call. Inherit D-39's three terms rather than re-deriving them, and add the two it does not
  cover: a committed regression corpus so any discovered failure becomes a permanent fixed case, and placement
  as **integration** tests rather than unit tests (they cross the OS boundary and will exceed the one-second
  unit budget).
  **Decided: permitted**, recorded as [D-41]DESIGN-NOTES.md#d-41. D-39's three terms carry over to any
  generator, and the two operational terms above are adopted. The engineer added a **second condition D-39 did
  not anticipate**: where non-determinism is inherent and outside our control -- multiprocessor scheduling,
  kernel completion order, thread-pool dispatch -- randomness is fair game, because a test depending on those
  cannot be seeded and pretending otherwise would make it deterministic in its inputs while still
  nondeterministic in what it observes. D-41 records the corollary that stops that becoming a loophole:
  inherent non-determinism excuses *irreproducibility*, never *unverifiability*, so an unreplayable test must
  prove it reached the state it claims to exercise. M17.1 is cited as the worked example of both halves -- its
  racing draft decayed into testing the easier state, and its replacement guards against exactly that.
  No dev-dependency is added here. D-41 permits randomized property testing without mandating a particular
  crate, and M17.3 went on to satisfy its terms with a seeded `SplitMix64` rather than `proptest` -- see that
  item for why.

- [x] **M17.3** -- Model the operation space as data: operation kind, buffer kind (owned / registered), file
  target kind (raw / shared / registered), claim-or-drop, drain-now-or-later, and handover state. A generator
  over *sequences* of these, each sequence checked against `RingContract`. The harness **must run under
  `windows-guard-alloc` with poison enabled**: if it does not, generated sequences are checked for conservation
  only, which discards every M15 detector at exactly the moment there is finally enough input to feed them.
  Address space is not a constraint on doing so -- measured at 8 KiB per allocation, about 8.6e9 allocations
  before 64 TiB, against a generative run needing a few million.
  Bound by [D-41]DESIGN-NOTES.md#d-41: the generator is **seeded, announced and pinnable**, so one number
  replays a whole run and varying it is how permutations are produced. Where a sequence's outcome also turns
  on scheduling or kernel completion order, that residue is legitimately random -- but D-41's corollary still
  applies, so any such sequence must **verify it reached the state it claims**, not merely fail to crash. Note
  the guard allocator's seed and the generator's are two separate knobs and must not be conflated in the
  announcement, or a replay will reproduce one and not the other.
  **Done:** [tests/generated_sequences.rs]tests/generated_sequences.rs. 128 sequences per run, ~650
  operations, all 15 reachable shapes, under the guard allocator and checked against `RingContract`.
  **No `proptest`.** D-41 permits randomized property testing without naming a crate. Its terms are that one
  number replays a whole run and is pinnable from the environment; `proptest`'s model is a persisted
  regression file plus per-case seeds, a different shape, and its headline feature -- shrinking -- earns its
  keep on long sequences. These cap at 10 steps and print every one, so a failure already arrives readable. A
  seeded `SplitMix64` over one `u64` serves the stated terms exactly. If the step cap is ever raised
  substantially, shrinking stops being redundant and the choice should be revisited; that condition is
  recorded in the file's module doc rather than left implicit.
  **Seeding verified end to end:** two runs pinned to the same pair of seeds produced byte-identical coverage;
  a different seed produced different sequences; both decimal and `0x` hex parse. The two seeds are
  independent knobs and both are announced with the replay command.
  **The generator is self-verifying, per D-41's corollary.** A green run proves nothing unless it shows which
  states it reached, so the test asserts every shape appeared, plus at least one deliberate drop, one
  mid-sequence drain, and one attach against a ring with work outstanding (#47's own axis). Sequence count is
  set *by* that assertion rather than by taste: at 48 the rarest shape (flush against a registered file, 3.3%
  of operations) would be missed about 1 run in 4000 and flake CI; 128 puts that at ~3e-10.
  **The first run found a defect in the generated program, not the ring.** It emitted a registered-buffer
  operation whose token was deliberately dropped, and `RegisteredBuffers`'s drop guard fired.
  `read_registered_raw`'s rustdoc already states that token "must be claimed", because claiming is the only
  thing that releases the use -- `Token`'s drop is deliberately empty (D-4), so dropping instead pins that
  buffer index for ever. Claim-or-drop is therefore a free axis only for owned buffers, which the generator
  now encodes alongside the existing rule that two live operations never share one registered buffer index.
  **Calibrated by sabotage, which also exposed a defect in the harness:** removing `RegisteredUse::drop`'s
  `outstanding` decrement -- D-32's own class of bug -- is caught and reported as
  `BufferStillInUse { index: 0, outstanding: 1 }` with the seed and the step that did it. The first attempt
  reported it as a bare `debug_assert` from inside the crate instead, because the registration's drop guard
  fires *while the error is being returned* and masked the diagnostic. The failure path now forfeits the
  registration deliberately -- the same leak-not-free choice the crate makes in release -- so the report
  survives. An instrument whose failure output names neither sequence, step, nor seed is barely an instrument.
  Note this sabotage is a *smoke test* that the harness is not inert, not the calibration M17.4 requires:
  it shows the oracle reports a fault injected into buffer accounting, not that the generator can emit the
  shapes that trigger the defects which actually shipped.

- [x] **M17.4** -- Calibrate the generator before its green result is allowed to count. Show that it
  rediscovers a defect known to be real -- #47's handover shape, or D-32's registration timing -- when the fix
  is reverted. Every M15 and M16 item carried a sabotage step and this milestone had none: a generator that
  cannot emit the shape that triggers #47 reports green for ever, and that green would feed straight into
  release confidence. Until this item passes, M17.3's result is uninformative by construction. Note M18.3's
  `cargo-mutants` run is a general form of the same calibration; this targeted version comes first because it
  is cheaper and aims at two known-real defects rather than synthetic mutants.
  **The calibration failed the generator, which is the entire reason this item exists.** With
  [D-20]DESIGN-NOTES.md#d-20's setup signal removed -- #47 exactly as it shipped -- the M17.3 generator
  reported **green**. It attached the completion event but drained with `try_pop`, and unconditional polling
  recovers every completion whether or not the ring ever signalled, so a lost wakeup was invisible to it. A
  generator can sample the right *states* and still be blind to the defect that lives in them.
  **Fix:** once an event is attached, a sequence now **waits for the wakeup it is owed** before draining, and
  a wait that times out with work still outstanding is reported. Waiting precedes draining rather than
  following it, because a backlog queued *before* the attach is reachable only through the setup signal --
  drain-first would consume it by polling and hide the signal's absence. Recorded as
  [D-42]DESIGN-NOTES.md#d-42.
  **Re-calibrated, and the detection is reliable rather than lucky:** with the sabotage restored the generator
  caught it in **10 of 10 runs on fresh seeds**, always within the first six sequences, and the reported trace
  is #47's own shape -- an operation queued, `attach completion event (queue had 1 outstanding)`, then a lost
  wakeup. With the fix in place, 10 of 10 clean runs at ~80 ms.
  **Second defect, D-32:** reintroducing the 0.1.2 use-after-free (build the SQE from an array dropped before
  submit) is caught as a hard `STATUS_ACCESS_VIOLATION`. Worth recording precisely, because it is *not* what
  0.1.2 did: there the same defect surfaced as a survivable `ERROR_NOACCESS` because the freed pages happened
  to still be mapped. The only difference is the guard allocator decommitting them, which is M15.1's stated
  purpose demonstrated against the real historical defect rather than a synthetic one. Note this one is caught
  at the registration *setup* boundary, which every registering test crosses, so it calibrates the allocator
  rather than the generated space -- #47 is the calibration that is specifically about the generator.
  **Process note:** an intermediate "failure" run was traced to a stale test binary left by the previous
  sabotage build, not to flakiness. Timestamps were checked against the rebuild before any conclusion was
  drawn from a direct binary run; a calibration that mistakes a stale artefact for a result is worse than none.

- [x] **M17.5** -- Make discovered failures permanent: every sequence the generator finds becomes a named,
  seed-free regression test committed alongside the corpus. A property test that finds a bug and then forgets
  it has bought a single debugging session rather than a guarantee.
  **The trap in this item is that the generator has found no defect in shipping code**, which makes "nothing
  to make permanent" look like a reason to defer. It is not: the first real failure is exactly the moment
  nobody wants to be inventing a corpus format, so the mechanism is built and proven now.
  **Corpus:** named, seed-free entries replayed verbatim through the generator's own executor, each carrying
  *why* it was recorded. Seeded with `regression_issue_47_backlog_at_handover` -- #47's shape exactly as M17.4
  reported it, kept in the shape it was found rather than tidied into a minimal one, because a corpus that
  rewrites what it was given cannot be trusted to have preserved the failing case. This is a real
  strengthening rather than bookkeeping: the generator reaches that shape within a handful of sequences on
  *most* seeds, and the corpus reaches it on *every* run.
  **Verified load-bearing:** with [D-20]DESIGN-NOTES.md#d-20 reverted the entry fails deterministically,
  reporting "reproduced a defect that was fixed", the recorded reason, and the step trace.
  **The calibration itself is now automated**, which M17.4's could not be. That procedure edits `src/`, so
  nothing in CI would notice if `wait_then_drain`'s wait were later "simplified" back into a poll -- the exact
  change that made the generator blind to #47 ([D-42]DESIGN-NOTES.md#d-42).
  `the_lost_wakeup_detector_fires_when_no_wakeup_is_owed` constructs the lost-wakeup condition against a
  *correct* ring instead -- attach, consume the setup signal, then wait again without draining, so no
  empty-to-non-empty edge can occur -- and asserts the detector reports it. Sabotaging the wait makes it fail,
  so the guard is real. A short 250 ms budget is used because that wait is *expected* to expire; `WAIT_MS`
  would add five seconds per run to learn the same thing.
  **Found while adding tests:** `temp_file` keyed its fixture on the process id alone, so the second test in
  this binary would have raced the first for one path -- libtest runs them as concurrent threads. Now tagged
  per test.
### M18 -- The population no runtime technique reaches

Population C, and the one that produced the two most severe findings of the M14 review round. `get_mut`
returning `&mut B` ([D-35](DESIGN-NOTES.md#d-35)) and `get` returning an unchecked `&B`
([D-36](DESIGN-NOTES.md#d-36)) are both defects of **what safe code is permitted to do**, not of what any code
path does. No fuzzer, oracle, allocator, poison scheme or chaos harness reaches them, because nothing has to
execute for the hole to exist. Both were found by review, which makes review a *primary* technique here rather
than a backstop -- and that deserves a written procedure instead of depending on who happens to read the diff.

Independent of M17; may run in parallel.

- [x] **M18.1** -- Audit every public type that hands out a borrow or an owned value, against one mechanical
  question: **what can safe code do with this, and does the registration or the kernel still hold anything it
  could invalidate?** `&mut Vec<u8>` permits `reserve`, `resize` and whole-value assignment; `&mut [u8]`
  permits none of them. Record the finding per item even when the answer is "nothing", so the absence of a
  hole is evidence rather than silence.
  **Done:** [DESIGN-NOTES.md]DESIGN-NOTES.md -> [Borrow-surface audit]DESIGN-NOTES.md#borrow-surface-audit-m181,
  nineteen items, each recorded including the sixteen where the answer is "no hole".
  **One finding, and it is the same shape as the two that prompted the audit.** `EventDelivery::ring` hands
  out `&Mutex<IoRing>`, and *any* `&mut IoRing` permits whole-value assignment, so safe code can replace the
  ring and silently stop delivery. Measured rather than argued: the replacement compiles, and a probe recorded
  one completion delivered before the swap and **none** after, despite four further operations completing on
  the replacement. The pool's wait holds a duplicate of the *original* ring's event, and nothing is ever
  attached to the new one. Recorded as [D-43]DESIGN-NOTES.md#d-43; the fix is **M18.6** below.
  **Worth carrying into M18.2:** the question that finds these is not "is this correct?" but "what else does
  this type allow?" Note also that the two obvious fixes do not work -- a `Deref`/`DerefMut` newtype still
  permits `*guard = ...`, and so does a `with_ring(|ring: &mut IoRing| ...)` closure. Anything that lets a
  `&mut IoRing` escape keeps the hole.
- [x] **M18.2** -- Turn M18.1 into a recurring obligation rather than a one-time pass, as a
  `DESIGN-INSTRUCTIONS.md` rule for this component: any change adding or widening a public borrow-returning
  method answers M18.1's question in the PR. Both C-population defects were introduced by ordinary,
  well-reviewed changes; what was missing was the specific question, not the diligence.
  **Done:** [DESIGN-INSTRUCTIONS.md]DESIGN-INSTRUCTIONS.md -- the repository's first, so it is kept tightly
  component-scoped. It states the question, what counts as answering it (three points, with the M18.1 audit's
  nineteen rows as worked examples), and that "no hole" is a complete answer that must still be written down.
  **A document alone would not have made this an obligation**, which is the item's own point: the rule it
  replaces was "remember to ask", and that failed three times. So the trigger is mechanical.
  [BORROW-SURFACE.txt]BORROW-SURFACE.txt is a generated inventory of every public function in `src/` whose
  return type carries a borrow; `tools/check-borrow-surface.ps1` regenerates it and fails on any
  disagreement, wired into CI beside the existing `encoding` and `baseline` sanity jobs -- whose own header
  describes this same restatement-drift problem, so the pattern was already established here rather than
  invented.
  **Scope of the trigger, chosen deliberately:** a reference *or* a lifetime-carrying wrapper. A `&`-only
  scan would have missed `Batch<'_>` and `RingScope<'_>` -- which is precisely where M18.6's fix lives, so the
  narrower scan would have gone blind to the surface it was written for. Seven entries today.
  **Verified load-bearing:** adding `pub fn sabotage_ring(&mut self) -> &mut IoRing` -- D-43's exact shape --
  makes the check fail and print the question. It checks *shape*, not correctness, and the instructions say so
  plainly: it cannot tell a safe accessor from a dangerous one, and tuning it until it stops firing would be
  the failure mode to guard against.
  **Found while writing it:** `.Count` on a `Where-Object` pipeline is a `StrictMode` error when the result is
  empty or a single item -- the check crashed rather than passing on its first clean run. Fixed with `@(...)`.

- [x] **M18.3** -- Run `cargo-mutants` over the crate and triage the surviving mutants. There is a measured
  rate to justify it: this branch produced two vacuously-passing tests (the M11.2 idempotence pair, which
  asserted through a freshly attached event and so could not observe a detached one) and one `debug_assert`
  that could never fire (`Option::take` had already emptied the slot it checked). Both are exactly what a
  surviving mutant looks like.
  **Result:** 306 mutants in 32 minutes -- 182 caught, 48 missed, 7 timeouts, 69 unviable. Counting timeouts
  as detections (a mutant that hangs the suite fails CI as surely as one that trips an assertion), that is
  **189 of 237 viable mutants caught, 79.7%**. Triaged into six categories in
  [MUTATION-SURVIVORS.md]MUTATION-SURVIVORS.md, which is M18.4's input.
  **It found a third vacuous test, which four review rounds had read past.** Both `Arc<[u8]>` tests in
  `src/buf/tests.rs` compare `stable_ptr()` against *another call to the same function*, so returning a **null
  pointer** -- the buffer address this crate hands the kernel -- passes both. They assert self-consistency,
  never that the address is real. The neighbouring `&'static [u8]` test already shows the fix: compare against
  an independently obtained address.
  **And it caught the previous commit's author.** Every read-only accessor M18.6 added to `RingScope` survives,
  because no test calls any of them. The surface was added on the stated principle that a platform layer is
  not narrowed to its current caller, which stands -- but mutation testing showed within one commit that none
  of it is exercised.
  **A first reading of one survivor was wrong, and checking corrected it.** The `contract.rs` mutant deletes a
  match arm in `check_quiescent`, which looked like the oracle's own busy-buffer detection going untested. It
  is the **sort key**, not the detection: the test has one busy buffer, so ordering is unobservable. Recorded
  as equivalent-under-current-tests rather than as a hole.
  **Tooling, measured rather than assumed:** `cargo install cargo-mutants --locked` fails on this host --
  the locked `winapi` does not compile for `aarch64-pc-windows-msvc` (285 errors). Installing unlocked, from
  outside the repo so `rust-toolchain.toml`'s 1.98.0 pin does not apply to the tool, works. `--timeout 180` is
  needed because several mutants hang, and a cap derived from the ~5 s baseline is too tight for a suite whose
  own waits are 5 s. All of this is in the reproduction section of the survivors file.
  Note `cargo-mutants` has no `cargo_*` MCP tool, so it is the rare case where the terminal is the only route.
- [x] **M18.4** -- Resolve the survivors: strengthen the test, or record why the mutant is equivalent and
  harmless. Do not delete an assertion merely because a mutant survives it -- a dead assertion beside a live
  one is worse than none, which is why M14.3's was removed rather than repaired.
  **Result: 48 survivors down to 12, and the score from 79.7% to 94.9%** (219 caught, 12 missed, 6 timeouts,
  69 unviable of 306). 18 new unit tests; no production code changed. Verified by re-running `cargo-mutants`,
  not by assuming the tests would kill what they targeted -- which mattered, because four of them did not.
  **The vacuous `Arc<[u8]>` tests are fixed** by comparing `stable_ptr()` against `Arc::as_ptr`, an
  independently obtained address, exactly as the neighbouring `&'static [u8]` test already did.
  **Four tests failed to kill what they aimed at, and re-running found each one.** Worth recording, because
  every one of them looked correct: (1) the sort-order test used three busy buffers, and with the sort key
  deleted `sort_by_key` is stable, so `HashMap` order came out sorted anyway -- now eight; (2) `checked_index`
  is the *span* bounds check, not `get`'s, so an out-of-range `get` never reached it -- now submits a span;
  (3) `user_data() -> 0` matched because a fresh ring's first operation *is* zero -- now burns ids first, and
  then `-> 1` matched for the same reason, so it burns two; (4) `supports_raw` was asserted only in the
  negative, which a constant `false` satisfies. **A test written against a mutant is not verified until the
  mutant is re-run.**
  **Two survivors are provably unkillable**, argued rather than assumed: `as_hresult`'s `|` versus `^` operate
  on disjoint bit sets and are the same function; `with_injected_failure`'s zeroed `information` is
  unreachable because `result()` returns `Err` first. Recorded with proofs.
  **The rest are recorded with named reasons** in [MUTATION-SURVIVORS.md]MUTATION-SURVIVORS.md, separated
  into unkillable, host-blocked (two `supports -> true` mutants need an `Op` this host does not support, and
  widening a closed enum to satisfy a mutant would be inventing API for a test), queued (capability decoding,
  spawned as **M18.7**), and two `Debug` impls whose exact wording an assertion would only change-detect.
  "Hard to test" is written down as that, never as "equivalent".

- [x] **M18.7** -- Extract the capability flag decoding in `capabilities()` into a pure function over the raw
  `IORING_CAPABILITIES`, so it can be exercised with synthetic flag values. Spawned by M18.4: four mutants on
  `raw.FeatureFlags & FLAG != 0` survive because nothing can vary what `QueryIoRingCapabilities` returns, and
  the decoding of `supports_completion_event` in particular gates every completion-event path in the crate --
  [D-20]DESIGN-NOTES.md#d-20, and #47 lived downstream of it. A pure `fn decode(raw) -> Capabilities` plus a
  table of flag combinations kills all four and makes the one capability that matters testable in both
  directions, which no test can do today.
  **Done:** `decode(&IORING_CAPABILITIES) -> Capabilities` split out of `capabilities()`, which now does the
  syscall and nothing else. Pure refactor -- no behaviour change -- plus five tests, and a round-trip test
  tying `decode` back to the query so the split cannot drift.
  **All four mutants killed**, verified by a scoped re-run: `capability.rs` reports 12 mutants, 9 caught and
  3 unviable, none missed. The killing cases fall straight out of the pure function: an empty mask kills
  `& -> |` (because `flags | FLAG` is non-zero whatever `flags` holds), a mask carrying exactly one named flag
  kills `& -> ^` (which clears the very bit it is testing), and any positive case kills `!= -> ==`. An
  unknown-feature-bit case is included because this crate has already been surprised once by a Windows
  reporting a version it did not name.
  **Crate-wide: 307 mutants, 221 caught, 10 missed, 6 timeouts, 70 unviable -- 95.8%**, up from 79.7% before
  M18.4.
  **Recorded a caveat on the headline number rather than quoting it flat.** Two mutants -- `Batch::require`
  and `IoRing`'s `Debug` -- were reported caught in one run and missed in another *with identical sources*.
  The suite has timing-dependent tests and `cargo-mutants` runs four jobs in parallel, so a mutant can be
  "caught" by a test that merely flaked. Both are now filed as unresolved rather than fixed, and
  [MUTATION-SURVIVORS.md]MUTATION-SURVIVORS.md says a single run's score is approximate.
  **`Batch::require -> Ok(())` was reclassified on the evidence:** `require` returns `Ok` exactly when
  `supports` is true, so on a host supporting every operation it is equivalent *in practice*. Filed under
  host-blocked rather than provably-unkillable, because the equivalence is a property of the host, not the
  code -- and its single "caught" result is consistent with the flake above.

- [x] **M18.5** -- Document the strategy as a whole in [DESIGN-NOTES.md]DESIGN-NOTES.md: the three defect
  populations, which technique covers which, and -- most importantly -- **what none of them cover.** Two of the
  eight defects came from spikes against the real kernel, and no oracle, generator or allocator would have
  produced either, because both were cases of Windows behaving differently from the assumed contract. Record
  spikes as a budgeted technique for each new Win32 surface rather than something that happens when a test
  mysteriously fails.
  **Done:** [DESIGN-NOTES.md]DESIGN-NOTES.md -> [Testing strategy]DESIGN-NOTES.md#testing-strategy-m185,
  plus [D-44]DESIGN-NOTES.md#d-44 making the spike rule citable from the decision index rather than reachable
  only through prose.
  **The item's own premise needed a correction, and checking found it.** "No allocator would have produced
  D-32" is not quite true: the guard allocator *does* catch it, as a hard `STATUS_ACCESS_VIOLATION`, measured
  in M17.4's calibration. The accurate statement is sharper and is what got written down -- every technique
  here checks this crate's code against **this crate's stated contract**, and in both spike-found defects the
  stated contract was the thing that was wrong. They detect a *consequence*, on a path some test already
  walks; a spike produces the platform knowledge that decides what the code should be, and is the only
  technique that runs **before there is code to test**.
  **Two things the write-up records that no single milestone did.** First, M15 and M16 finding nothing is not
  reassurance: they are passive, so zero findings meant the detectors had only seen the twenty-odd
  hand-written scenarios that already passed -- which is why M17 exists to feed them. Second, **every one of
  these instruments was wrong the first time and only sabotage found it**: M17.3's generator reported green
  with #47 reintroduced, four of M18.4's mutation-killing tests did not kill their mutant, and M15.2's poison
  inverse was fabricated twice. Budget the calibration, not just the instrument.
  Also records the two rejected techniques -- a mock `IoRing` and PageHeap -- so neither is re-proposed as an
  obvious win, and a per-technique table of what each actually found rather than what it was hoped to find.
- [x] **M18.6** -- Narrow `EventDelivery`'s ring surface so safe code cannot replace the ring out from under
  the pool's wait. Spawned by M18.1, recorded as [D-43]DESIGN-NOTES.md#d-43, and **measured**: the
  replacement compiled and silently stopped delivery -- one completion before the swap, none after.
  **The two obvious fixes do not work.** A `Deref`/`DerefMut` newtype still permits `*guard = ...`, and so
  does handing a `&mut IoRing` to a closure. Closing the hole means never letting a `&mut IoRing` escape at
  all.
  **Done:** `EventDelivery::ring -> &Mutex<IoRing>` is replaced by `EventDelivery::scope -> RingScope`.
  The exposure rule is stated rather than ad hoc: **every read-only part of `IoRing`, plus batch
  construction -- and nothing that can retarget the ring or steal the pool's completions.** So `batch`,
  `outstanding`, `info`, `version`, `supports`, `supports_raw` and the registered counts; deliberately no
  `try_pop` ([D-21]DESIGN-NOTES.md#d-21 makes the pool the single drainer), no `completion_event` (two
  waiters on one ring), no `run_down`, and no `&mut IoRing`.
  **Reviewed for sizing before starting, and one item was right.** Nine call sites across six files, all of
  which stop compiling the moment `ring()` is removed -- so any split would have produced a non-compiling
  intermediate commit, and the add-alongside-then-remove shape would have left the hole open across commits.
  Larger than the item text implied, though: it also forced `handover.rs`'s `submit_wave` to split into a
  `queue_wave` that fills a caller-owned `Batch` (four plain-ring call sites kept their old shape), because
  `Batch::submit_and_wait` consumes the batch and the scope hands out a `Batch` rather than a ring.
  **Proved by construction, not by review**, which is the whole point of M18: a `compile_fail` doctest
  asserts the replacement no longer compiles. That doctest was itself verified -- adding a `DerefMut` impl
  makes it **fail**, so it is passing for the right reason rather than on a typo in its own setup, and that
  same experiment is the empirical proof of the "a `Deref` newtype would not close this" claim above. It is
  paired with a normal doctest sharing the setup, since a `compile_fail` example passes on *any* error.
  **Incidental fix:** call sites were inconsistent about mutex poisoning -- some `expect`, some
  `unwrap_or_else(PoisonError::into_inner)`. `scope()` absorbs poisoning internally, matching what the wait
  callback's own drain already did, so the question no longer reaches callers at all.
  Verified: 148 tests pass, both affected examples (`model_a_delivery`, `epoch_log`) still run to exit 0.