Skip to content

windows-ioring-sys: durability, a seeded response-space resolver, and the delivery-stall fix (M20-M26) - #108

Merged
MikeGrier merged 98 commits into
mainfrom
mikegrier/morechanges
Sep 25, 2026
Merged

MikeGrier merged 98 commits into
mainfrom
mikegrier/morechanges

Conversation

@MikeGrier

@MikeGrier MikeGrier commented Sep 25, 2026 •

Copy link
Copy Markdown
Owner

Releases the windows-ioring-sys work from M20 through M26, plus the new win-numa-sys
crate that M22.3 split out. 48 checklist items across ten milestones.

What this is

Three themes, roughly in the order they were worked.

Durability (M25). The epoch log opens its file NO_BUFFERING | OVERLAPPED over a
written extent, records get a sector stride at both the writer and the reader, and a
commit is measured as submit / blocking / deferral rather than as one number. The crate
states that the ring, not the log, is the durability unit, and that it owns one flush
rather than durability groups.

Testing against a space, not an observation (M26). The central idea: a fake that
models what Windows does freezes one run's testimony, so instead
RESPONSE-SPACE.md specifies what Windows is
permitted to do, and a seeded resolver picks one resolution out of that space. The
assertions are then about this crate under that resolution, never about the kernel -- which
dissolves the mock objection rather than working around it. The space must be wider than
anything observed
, and deriving it from what we have seen would close the trap again.

Correctness repairs (M20-M24). Bounded waits everywhere that could hang rather than the
two that were named; an expired wait treated as a result rather than an error; Drop guards
kept silent while already panicking; a cache-level question asked properly instead of
filtering on L3.

The one behavioural fix a consumer should read

EventDelivery took an already-signalled event and armed its thread-pool wait afterwards.
SetThreadpoolWait documents the opposite order -- "you must re-register the event with
the wait object before signaling it each time to trigger the wait callback"
. Because the
event is auto-reset the lost signal was consumed rather than left pending, and because it is
edge-triggered on the queue going empty-to-non-empty, a ring whose queue was already
non-empty had no second wakeup coming. The delivery stalled permanently rather than late.

Measured on the reproducer: 0 failures in 3600 runs, against 5-in-600 and 2-in-600 for the
two co-running triggers before.

Breaking changes

  • feat!: create win-numa-sys and move NumaBuffer into it -- NumaBuffer now lives in
    its own crate, and NumaBuffer::new takes Option<NumaNode> rather than Option<u32>.
  • fix(ioring)!: ask which cache level partitions the machine, never filter on L3.
  • feat(ioring)!: D-55 -- the ring owns the pending inventory -- see the note below;
    this commit records the decision and adds Pending<T, X>, but the generic IoRing it
    announces has not shipped.

Two things to check on the release PR

Both are known, and neither is fixed by editing this branch.

  1. The D-55 commit's BREAKING CHANGE footer is written in future tense -- "IoRing will
    become generic over the token payload"
    . release-please will render that under
    BREAKING CHANGES for this release, describing a break that has not happened yet (it is
    M28, unstarted). The commit is ~90 back and pushed, and rewriting it would dangle the
    hashes this branch's split-provenance commits cite, so the CHANGELOG is the right place to
    correct it -- please adjust the wording in the release PR rather than taking it as-is.

  2. win-numa-sys bootstrapping (the branch side is fixed; the release PR side needs a look). It was in release-please-config.json but never in
    .release-please-manifest.json, and has no tag. 29a6ebd1 adds it at 0.0.0 so the
    breaking commit that created it computes 0.1.0 under bump-minor-pre-major, matching
    its Cargo.toml. This matters beyond tidiness: windows-ioring-sys carries
    win-numa-sys = { version = "0.1.0", path = ... }, a versioned path dependency on a crate
    that is not on crates.io, so a release that did not also publish it would fail at
    cargo publish. The publish workflow already waits for workspace siblings on the index,
    so ordering is handled once both tags exist. Please confirm the release PR actually
    proposes win-numa-sys 0.1.0.

Verification

Every configuration builds and tests clean -- default, --all-features,
--no-default-features, --features kernel-seam, debug and release -- along with doctests
and the three guard scripts (check-encoding, check-ring-tests, check-borrow-surface).

The sabotage manifest is at 53 cases, all behaving as declared, including the
expect: "survives" controls that are what make the rest meaningful. That sweep earned its
keep on this branch: it caught two cases gone stale against a reshaped array, which would
otherwise have reported caught for a patch that no longer applied.

Not in this release

M27 (what this crate owes the topology planner) is parked behind a cross-component gate on
topology-planner. M28 (making IoRing generic over the token payload) is unstarted, and
is deliberately left for the next release rather than bundled into this one.
Two commits close the branch side. 29a6ebd1 adds the manifest entry. 3fbb0a51 then
registers the crate in publish-crate.yml, which CI caught and the manifest change alone
did not cover: it was missing from the workflow_dispatch choices and -- the one that
matters -- from the workspace_crates registry the sibling-dependency wait reads, so
windows-ioring-sys could have raced its tag instead of waiting for it. Both guards are
green now.

Mike Grier and others added 30 commits September 19, 2026 22:32
A read-only review of examples/epoch_log and the durability surface it composes,
recorded as a design session and queued as work items.

Ten items across three milestones: correctness repairs (M21), the submission and
arena shape (M22), and the ring as a durability domain plus storage affinity
(M23). One addendum to the already-queued M20.6, which the review strengthens
rather than replaces: D-47 kept the half of D-24 saying the barrier reaches every
operation outstanding on the ring, so alternating rings bounds what a commit can
be dragged into -- a throughput argument, not only a correctness one.

Nothing was built, run, or measured. Every finding is from reading the source and
the recorded decisions, and each item states whether it rests on a measurement or
on reasoning.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Re-planning, not new work. M20 was queued on the basis that no defect was found
in ring_copy -- Policy::select already degrades and reports it. SH-4.12 later
superseded that basis by finding two defects in the same function: it selects on
DomainKind::Cache { level: 3, .. } instead of asking outermost_partitioning_cache(),
so it can produce overlapping ring domains where two cache kinds report at level 3,
and degrades silently on a host whose outermost partition sits at another level.

Neither checklist said so, and both M20.1 and M20.3 land on that function and that
rule. M20.1 sweeps the L3 rule into policy.rs doc comments, which done first would
describe a rule the code does not implement; M20.3 would pin the selection arm
SH-4.12 rewrites. Reciprocal callouts now name the ordering from both sides, and
M20's stale header is corrected adjacent to the claim it makes.

M20.2 and M20.4 are independent and can proceed; M20.6 is gated the other way, on
M22.1.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…reachable"

The section said mapping a file handle to its backing device's NUMA node has "no
clean user-mode path" and means walking volume to disk to device instance for
DEVPKEY_Device_Numa_Node. That is wrong on mechanism: FSCTL_QUERY_VOLUME_NUMA_INFO
is documented, takes a file or directory handle directly, and returns the node in
one call. GetNumaNodeNumberFromHandle is recorded as the other path, together with
the fact that its FileNumaNodeInformation class is reserved for system use, so the
crate must not build on it.

The conclusion is unchanged and now rests on why it is actually true: the answer is
volume-granular rather than extent-granular, absent whenever the device layer
advertised no proximity domain, and meaningless where one volume sits on several
devices.

Swept "no clean user-mode path" and its siblings: 4 files, 3 updated, 1 left
deliberately (DESIGN-NOTES quotes the old wording to say it was wrong). The sweep
also found two sites stale in the OPPOSITE direction -- the spikes README and the
spike header both describe an instrument that "establishes nothing yet" and a
DESIGN-NOTES claim that no longer exists, each contradicted by its own body.

Re-planned while doing it: the item said to cite the spike as UNRUN and to state
that no measurement of either call succeeding on an ordinary NTFS file could be
found. F-1a of the source session had already smoke-run it -- both calls succeed and
agree. The item was written from F-1 without F-1a. What remains unmeasured is
narrower and is recorded as such.

Gate: cargo fmt --check clean, cargo clippy -p windows-ioring-sys --all-targets
clean. Run in the terminal because no cargo-mcp server is registered in this
environment.

Completed item: M20.4: Correct "What is not reachable" in DESIGN-NOTES.md -- the
file-handle-to-storage-node mapping is reachable on mechanism, and the conclusion
it supported now rests on volume granularity, absence, and spanned volumes instead.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
A shipping ARM consumer laptop (Snapdragon X2 Elite, 12 cores, no SMT) reports no
L3 cache domains at all -- L1 and L2 only, with L2 forming two domains of six that
agree with the two Module domains -- and zero Win32_NumaNode instances. Recorded so
the next reader inherits the datapoint instead of re-measuring it.

Placed beside the zero-NUMA-node observation these notes already carried, because
the pair is the point: one machine where the node is absent because a hypervisor did
not present it, one where it is absent on bare metal, and neither is unusual.

The measurement is cited rather than re-transcribed -- D-48 links Measurement M-1 in
the 2026-08-30 session instead of copying the probe output into a third place.

D-48 falsifies a justification and not the heuristic: these notes say the
last-level-cache domain "is meaningful on Intel and ARM too", which is false on this
part. That clause now carries a one-line adjacent marker, because a document that
states a measurement two paragraphs from the sentence it disproves contradicts
itself. The marker is deliberately NOT the restatement: rewriting the rule and
sweeping it across the README, lib.rs and policy.rs is M20.1, which is coupled to
SH-4.12 and must follow it.

Numbered D-48 rather than filling the D-46 gap, which is unused in this crate but
may be referenced from outside it.

Completed item: M20.2: Record the measurement itself as a decision in DESIGN-NOTES.md,
so the next reader inherits the datapoint rather than re-measuring: an ARM Windows
laptop with no L3 at all, and zero Win32_NumaNode instances, is the common consumer
shape now rather than an exotic one. This is the ARM sibling of the existing
zero-NUMA-node VM observation and belongs beside it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
DESIGN-NOTES.md opened with "This crate does not exist yet as compiled code" and
lib.rs's rustdoc said "Under construction", both long false. Neither is corrected
here -- both are removed, because a crate's status is derivative: CHANGELOG.md and
the git tags already own it, and any copy of it in prose is maintained only by
somebody remembering.

A first attempt replaced them with a drift-minimised status paragraph carrying no
version number and linking the artifacts. That was still wrong for the same reason --
"is published" is itself a copy of the release state, just one without digits in it.

The notes now say in one sentence that they deliberately state no release or
milestone status, and name what does, so the section does not get helpfully added
back. The rustdoc section keeps its pointers to the design record and the checklist
and drops the lifecycle claim.

The rule this follows is added to copilot-instructions.md in a separate commit.

Gate: cargo fmt --check clean, cargo clippy -p windows-ioring-sys --all-targets
clean, cargo test -p windows-ioring-sys --doc clean.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ifact owns

Rule 4 already governs measured numbers pasted into prose. It does not reach facts
that are DERIVED rather than measured: release and publication status, version
numbers, which milestones are done, whether a branch landed, how many crates or
tests exist. Each is owned by an artifact that is already authoritative, and each
copy in prose is maintained only by somebody remembering.

The rule names the tell that makes this class survive review: the copy cannot be
wrong at the moment it is written. It is accurate, which is why it gets written, and
nothing will ever say when it stopped being.

Four clauses: delete rather than update, because correcting re-arms the same hazard
with a fresh date; removing the digits is not enough, since "is published" is as
much a copy as "published at 0.3.1"; an absence may be worth one sentence so nobody
adds the section back; and it does not reach the primary record, where decisions,
measurements and rationale are owned.

Reconciled with FAIL FAST rule 6, which is the sibling rather than a contradiction:
that rule binds a count to a command when the claim must exist anyway, this one is
the prior question of whether to state it at all.

Numbered 6 rather than inserted next to rule 4, because rule numbers in this section
are cited by ID from seven files and renumbering would break every one. Rule 4 gains
a one-clause forward pointer instead.

Earned by the worked example the rule cites: windows-ioring-sys carried "does not
exist yet as compiled code" and "Under construction" through several releases, and
the first fix for it was itself a violation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…hat survived

The epoch-log committer justified its epoch-order debug_assert with D-24's claim
that a drained operation holds back what is pushed behind it. D-47 withdrew exactly
that half, and the same file's module header already carried the correction -- so
the file disagreed with itself, three lines of scrolling apart.

The assertion is unchanged, because it was always sound and only the reason was
wrong. Every commit carries CoversPrecedingOperations, so commit N is outstanding
when commit N+1 is reached, and D-47's SURVIVING half -- no operation queued before
a drained flush was ever observed completing after it -- is what orders them. The
comment now also says what it does not rest on, so a later reader cannot helpfully
restore the withdrawn reasoning.

Sweep re-run before committing, as the item required: 21 matches across 14 files.
1 violation, fixed. 1 historical site marked rather than rewritten -- the 2026-08-28
session glosses D-24 as a stall holding operations against unrelated files, which is
what it concluded on that date, so the gloss stays and a note beside it names the
half later withdrawn. 2 false positives left alone (both are about holding a token
or an instrument, not an operation). 17 already correct.

The item predicted 17 matches across 10 files. Both were low, and the file count
needed a command rather than an eye: rg groups the two design-session files under a
single header, which undercounts by one on a casual read.

Gate: cargo fmt --check clean, cargo clippy -p windows-ioring-sys --all-targets
clean. Verified by running the example in a debug build, where the debug_assert is
live: it completed, all four epochs reported durable in order, and the negative
control still caught the corrupted record.

Completed item: M21.1: Correct the last site that still asserts D-24's withdrawn
half -- the epoch-order assertion in the epoch-log committer, whose justification
cited the hold-back claim D-47 removed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…plies

try_pop says "empty right now" and submit_and_wait deliberately promises
nothing about poppability, because its timeout may have expired. Neither
answers "give me the completion I just caused", so every caller wrote that
join themselves -- five sites, four shapes, two of them unbounded spins that
turn a slow completion into a hung process.

The crate had not published one because it could not choose the wait: the
completion event is auto-reset with exactly one waiter per ring (D-21), so a
wait this crate picked could consume an edge the caller's own loop was
entitled to. Taking the wait as a parameter moves that obligation to the only
party who can discharge it.

Added: IoRing::pop_within (convenience, uses SubmitWait), pop_within_with
(?Sized, so a trait object works), the CompletionWait trait, RingWait, and
SubmitWait. CompletionWait's contract is deliberately weak -- returning early
or spuriously is always permitted, since D-19 already makes a wake with
nothing to pop normal -- so an implementation cannot be subtly wrong about
when to return, only wasteful.

RingWait is the ring narrowed to block and outstanding. A waiter that could
pop would consume the completion its own caller is waiting for; one that could
submit would queue work nobody asked for. Same narrowing, same reason, as
RingScope under D-43.

Borrow question, answered though the check reports the surface unchanged (7
entries): RingWait is only ever passed in, never returned. Safe code reaching
it can call block and outstanding and nothing else; the ring it borrows is held
exclusively for the call; and the narrow type is the point, not an accident.

Measured while building it, now documented on RingWait::block: SubmitIoRing
answers E_INVALIDARG (0x80070057), not a timeout, when asked to wait for a
completion the kernel has no pending operation for. The precondition holds
structurally -- pop_within_with checks outstanding() before consulting the wait.

Also closed a panic path before it shipped: the first draft added the timeout
to an Instant directly, which panics on overflow, so Duration::MAX would have
aborted the process. Now checked_add, with an unrepresentable deadline treated
as one that never arrives.

Sabotage-verified, because a test that cannot go red proves nothing. Removing
the sub-millisecond clamp turns the_wait_is_never_handed_a_zero_timeout red.
Removing the nothing-can-arrive early return turns two tests red AND takes the
full 30-second bound instead of finishing instantly, which is the behaviour
that early return exists to remove.

Gate: cargo fmt --check clean, cargo clippy -p windows-ioring-sys --all-targets
clean, full cargo test for the crate green including doctests, epoch_log example
runs end to end, check-borrow-surface and check-commit-scope both clean.

Completed item: M21.2: Publish a bounded pop and the wait it is generic over,
then remove the two unbounded spins.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…produce

The append loop tested `appended % EPOCH_SIZE == 0` after the match, so the
condition was still true on a WouldBlock retry, where `appended` had not moved.
The match now yields a closed_an_epoch value every arm must produce, which keeps
"this append closed an epoch" local and stops a later arm falling through into a
commit.

M21.3 and review finding C-3 both predicted a latent bug here -- unreachable at
the sample constants, armed for anyone raising EPOCH_SIZE past SLOTS. That is
wrong, and measuring it is what settled it. The retry path was instrumented to
report when the old shape would have committed, and run at EPOCH_SIZE of 6, 8,
12, 16 and 24: it fired zero times, including at every value past SLOTS.

The reason sits three blocks from the trigger. The predicate is true at exactly
two moments -- before the first append, and immediately after a commit -- and the
arena is empty at both, because the commit waits for a covering flush that
retires every outstanding write. An append is never refused at an epoch
boundary, at any constants.

So this is a coupling change, not a fix, and the difference is the point: the old
trigger was safe because of an invariant nothing stated, three blocks away; the
new one cannot fire because of where it is written.

Swept the claim rather than only the code. C-3 in the review session carried the
same wrong prediction and now carries the correction adjacent to it, as does the
M21 milestone header, which had called it a latent trigger.

Gate: cargo fmt --check clean, cargo clippy -p windows-ioring-sys --all-targets
clean, full cargo test for the crate green, epoch_log example output byte-for-byte
unchanged at the sample constants.

Completed item: M21.3: Key the epoch commit off a completed append rather than
off the counter, so the trigger cannot fire on a pass that appended nothing.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The doc on Committer::claim said "A failed commit advances nothing", which
reads as a permanent verdict. It is not: every commit here is a covering
flush, so commit N+1 reaches epoch N's writes -- queued before it -- and
observing N+1 makes N durable after all. What makes a record durable is a
flush that covered it, not the identity of the flush named for its epoch.
The doc now says that, and says what a caller must not read into a failure:
not "epoch N is lost", but "not yet".

The sample had no tests at all -- examples are not test targets by default,
so cargo test compiled this one and ran nothing. Binding the claim meant
adding test = true to the [[example]] entry first, which is what makes any
of the sample's policy testable rather than just this item's part of it.

The failure path cannot be reached by running the sample, because a flush
against a healthy temp file does not fail. The fault-injection seam is the
only way in, so four of six tests are gated on that feature and two run by
default -- following the precedent and the reasoning already written down in
tests/fault_injection.rs, including that CI's --all-features job is what
stops gated tests from being tests that never run.

An assumption caught by asserting it: the first version expected an injected
ERROR_ACCESS_DENIED to surface as ErrorKind::PermissionDenied. It surfaces as
Other, with the HRESULT preserved in IoRingError. Corrected to assert the
Win32 code, matching what fault_injection.rs already asserts.

Sabotage-verified in both directions, and the two produce DIFFERENT failure
sets, which is what shows the tests discriminate rather than all keying on
one fact. is_durable returning true unconditionally fails 4 of 6; is_durable
requiring an exact watermark match fails 2 of 6.

commit_and_pop is three lines because M21.2 published IoRing::pop_within.
Without it every test here would have carried its own bounded wait.

Gate: cargo fmt --check clean, cargo clippy --all-targets --all-features
clean, cargo test --all-features green, default-feature run green with the
gated tests skipped, epoch_log still runs end to end.

Completed item: M21.4: State what a failed commit does to durable_through,
and bind it with tests in both directions.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… so far

A running ledger of findings produced while IMPLEMENTING the M21 items, kept
separate from the 2026-09-19 session that recorded the review which produced
them. Committed rather than held in a session, so the next review pass starts
from it instead of rediscovering it.

Nine findings F-1..F-9 so far, and the headline is uncomfortable: two of this
pass's corrections were to the REVIEW rather than to the code it reviewed.
Both were reachability claims -- statements about which states a program can
enter -- and neither could have been settled by reading. C-3 asserted a latent
bug that measurement showed was unreachable at any constants; a test asserted
an error kind the crate does not produce.

The rule that falls out, for the next pass: a reachability claim belongs in a
review as a question with its experiment attached, not as a finding. It costs
the same to write and does not need correcting afterwards.

Also records four open questions this pass raised and did not answer, none of
them queued as checklist items yet, each flagged for the next pass to settle
by measuring rather than by reading.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…named

await_flush and await_writes blocked in submit_and_wait for WAIT_MS, ignored
that it had returned without a completion, and went round again forever. Both
now carry a deadline and raise TimedOut -- the policy event_loop.rs already
documented, on the grounds that a measurement harness which hangs reports
nothing, which is worse than one that fails.

The item named two loops. A census by command over every .rs outside target/
and the spikes found four, plus two more of a related shape:

  - strategy.rs await_flush and await_writes, the two named;
  - failure_paths.rs and kernel_span.rs, each with a helper called await_one,
    byte-identical to the other, neither named by the item or the review;
  - batch/tests.rs, where two registration waits were a single try_pop -- the
    flake shape pop_within documents -- in a file whose third such wait
    already used the helper. One predicate, three sites, half-converted.

That two independently written helpers share the name await_one is the tell:
it is what people call this operation, which is the argument for it belonging
to the library rather than to each caller.

Sabotage-verified, and it showed the milestone compounding. Making classify
stop filing flush results leaves await_flush waiting for a completion that is
never recorded, where an unbounded loop hangs forever. It failed with "timed
out after 30s waiting for a commit's flush" -- in TWO seconds, because
pop_within's nothing-can-arrive early return answers at once on a quiesced
ring. The bound makes the failure possible; M21.2 makes it quick.

Lane::classify is factored out of Lane::drain so the bounded waits can file a
completion they blocked for without a second copy of the claim-then-check
logic. A remaining() helper and one timed_out() constructor stop the two waits
describing the same condition two ways, and WAIT is derived from WAIT_MS.

This completes M21.

Gate: cargo fmt --check clean, cargo clippy --all-targets --all-features clean,
cargo test green under both --all-features and default features, epoch_log runs
end to end.

Completed item: M21.5: Give the harness's wait loops a bound, and collapse the
hand-written waits onto the bounded pop.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
An independent review of the M21.2 surface found a High-severity defect in it,
a pre-existing one sharing its root cause, and the reason neither was caught.

SubmitIoRing reports an expired wait as ERROR_TIMEOUT (0x800705B4) -- a FAILURE
HRESULT. RingWait::block passed it through check(), so pop_within returned Err
on every real timeout and never the Ok(None) it documents. All six converted
call sites treat Err as fatal and Ok(None) as the timeout signal, so the
ErrorKind::TimedOut mapping they document was dead code on the only path that
produces it.

run_down is the second victim, and worse: it polls in 50ms steps with the same
check(), so any operation slower than 50ms made rundown return Err with work
still outstanding -- and Drop then asserted and called CloseIoRing anyway,
which is exactly the hazard rundown exists to prevent. One wait_outcome helper
now classifies a wait-only SubmitIoRing result for both callers.

The rundown loop stays unbounded on purpose. Every SQE that queues produces
exactly one completion (M10.2), so it terminates; blocking until that holds is
the safe failure, and closing the ring early is not.

Also: a finite bound above ~49.7 days saturated onto u32::MAX, which is Win32
INFINITE, and CompletionWait invites WaitForMultipleObjects implementations.
Clamped to MAX_WAIT_MS. And the trait never said how to report an expired wait,
so any third-party implementation forwarding its underlying result would have
reproduced the defect independently; it now says an expired bound is Ok(()).

WHY NO TEST CAUGHT IT. The M21.2 tests drive the loop with a wait that never
enters the kernel -- deterministic, and it leaves the Win32 interaction
untested. Replacing RingWait::block's entire body with an unconditional error
left the whole suite green.

Closing that needed an operation still pending when the bound expires, and
three attempts failed first, each measured: a buffered read completes from
cache in 3-5us at any size; a flush over 512MiB of dirty cache takes 3us
because the lazy writer got there first; and an unbuffered 256MiB read also
takes 3us, because A HANDLE WITHOUT FILE_FLAG_OVERLAPPED IS SYNCHRONOUS and
the read completes inline during submit. Only unbuffered AND overlapped is
genuinely pending.

That last one is a finding about the whole suite, recorded as F-13: every
fixture in this crate opens a synchronous handle, so the tests have been
exercising inline completion almost exclusively.

tests/bounded_pop.rs covers it with five tests over a 128MiB unbuffered,
overlapped read. Each asserts outstanding() > 0 beside the expected answer, so
a machine fast enough to finish early fails loudly rather than passing
vacuously. Nothing asserts an upper bound on elapsed time: Windows default
timer resolution is ~15.6ms, so a 5ms bound routinely takes 14-19ms.

Sabotage-verified twice: reverting the timeout mapping turns all five red, and
reproducing the review's original mutation now turns three red where before it
turned none.

The review's fifth finding -- check-borrow-surface.ps1 is blind to trait
methods and to borrows in parameter position, confirmed by experiment with a
control -- is queued as M21+.1 rather than fixed here, since widening the check
obliges a borrow-question answer for every entry it newly reports.

Gate: cargo fmt --check clean, clippy --all-targets --all-features clean, full
suite green, check-borrow-surface and check-commit-scope clean.

Completed item: M21.6: Fix the four defects an independent review of the M21.2
surface found.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
It inspected only the text after the last -> on lines matching `pub fn`.
Trait items are declared `fn`, not `pub fn`, so nothing inside any `pub trait`
was ever examined; and a borrow-carrying type in PARAMETER position was
invisible anywhere. CompletionWait::wait is both at once, which is how the
M21.2 surface changed without the check saying a word.

Four entries appeared, and one predates the widening by months:
IoRingErrorExt::as_ioring_error returns Option<&IoRingError> and had simply
never been inventoried. The other three are CompletionWait::wait, Batch::new
and EventDelivery::new, the last two carrying an explicit lifetime in a
parameter. The borrow question is answered for all four in DESIGN-NOTES.md
BEFORE the inventory was regenerated, as DESIGN-INSTRUCTIONS.md requires.
None is a hole. 7 entries -> 11.

A plain &T parameter is deliberately not reported: that would list the whole
crate and mean nothing. Only an explicit lifetime counts -- the wrapper whose
lifetime the crate chose, not a reference the caller lent us. The audit says
plainly that this is a heuristic and would miss a &dyn Trait parameter with no
named lifetime.

Verified with five probes, restored afterwards. The negative control is the
one that matters, since a check that fires on everything is as useless as one
that fires on nothing: a pub fn taking a plain &Completion stays silent, while
a trait method returning &[u8] and a pub fn taking &mut RingWait<_> are both
now caught, and the old control still passes.

The probes also found a latent bug in the checker. A one-line body --
pub fn f() -> &[u8] { &[] } -- never satisfied the "line ends with {" test, so
the accumulator ran past the end of the file. The old script did not crash on
that only because it never indexed the lines again; it silently swallowed the
following lines instead, so it may have been skipping real signatures. Signature
termination is now "the accumulated text contains a {", and the return type is
truncated at that brace.

Swept while here: the header and the failure message both said THREE shipped
defects of this shape and listed D-35, D-36, D-43. It is four, and has been
since D-45. M19.3 swept that exact count through DESIGN-INSTRUCTIONS.md and
missed this file -- restatement drift landing on the tool built to stop a
different kind of it.

Gate: check-borrow-surface clean at 11 entries, cargo fmt --check clean, no
Rust source touched.

Completed item: M21+.1: Teach check-borrow-surface.ps1 the two shapes it was
blind to: methods of a pub trait, and borrows in parameter position.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…s fix

One `cargo test --all-features` run reported FAILED: 4 passed, 1 failed in a
target finishing in 2.12s, which matches tests/bounded_pop.rs by shape. It has
not reproduced in 19 runs since: 15 isolated runs of that target and 4 full
--all-features suite runs, all green.

The failing test name and panic message were NOT captured -- the summary line
was read and the run discarded. That is the handling defect here, and it is
recorded as such, because re-running cannot recover what was thrown away.

The plausible mechanism is unconfirmed: these tests need a read still pending
when a short bound expires, and get it from a 128 MiB unbuffered overlapped
read. NO_BUFFERING bypasses the system cache but not the drive's own, so a run
where the device serves it unusually fast would make popped.is_none() false.
Each test asserts outstanding() > 0 beside that assertion precisely so such a
run fails loudly rather than passing vacuously, which is consistent with what
was seen.

Recorded rather than fixed because the robust shape is an operation that CANNOT
complete -- a read on an overlapped named pipe nobody writes to -- which removes
the timing dependence instead of widening a margin. That needs
Win32_System_Pipes in the dev-dependency features and confirmation that IoRing
accepts a pipe handle at all. Both were probed; the feature is absent, so the
question is open. Queued as M22+.1, with the retry fallback named in case pipes
do not work.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…vice

The tests needed an operation still pending when a short bound expired, and
got it from a 128 MiB unbuffered overlapped read. That is a margin, not a
guarantee: FILE_FLAG_NO_BUFFERING bypasses the system cache but not the
drive's own, so the test was asking "will this device take longer than 5ms?"
-- a question about someone else's hardware, which can answer differently on
two runs of the same machine. It did, once.

The read is now issued against an overlapped named pipe nobody has written to.
It is pending because no byte exists to satisfy it, and it completes exactly
when the test writes one. There is no device, no cache, and no margin in the
question.

Both unknowns were probed rather than assumed: IoRing does accept a pipe handle
for read_raw, and pop_within(20ms) against an unwritten pipe returns Ok(None)
with outstanding == 1. Win32_System_Pipes joins the dev-dependency features;
PIPE_ACCESS_INBOUND is not re-exported where the module name suggests, so it is
a named local constant as FILE_FLAG_NO_BUFFERING already was here.

Where a delay is still needed -- run_down polls in 50ms steps, so forcing it to
observe an expired poll means releasing later than that -- it comes from a
thread::sleep, whose guarantee runs the safe way round: a sleep may overshoot,
never undershoot. No assertion depends on an operation FINISHING within a bound.

Verified: 25 consecutive runs of the target, 3 full --all-features suite runs,
and both feature configurations, all green. Both sabotages still bite exactly
as before the rewrite -- reverting the timeout mapping turns all 5 red, making
RingWait::block always fail turns 3 red. That is the assertion that mattered:
a deterministic test that had lost its discriminating power would be a worse
outcome than the flake it replaced.

Also 11x faster (0.20s against 2.26s) with no 128 MiB fixtures.

On the deferral itself: this was filed as M22+.1 with an UNRESOLVED-TEST-FAILURES
entry on the grounds that it did not belong in a push of finished milestones.
That is a scheduling preference, not a blocking factor, and the PRIME DIRECTIVE
is explicit that only the latter justifies deferring. The mechanism was
understood when it was filed and the two open questions were each one probe
away. The entry moves to RESOLVED-TEST-FAILURES.md in this commit.

Completed item: M22+.1: Make bounded_pop.rs independent of how fast a device is.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
A unit test whose outcome depends on system load is not a unit test. Four of
the M21.2 unit tests carried five wall-clock assertions between them. All five
are gone; each is replaced by a causal assertion, or deleted where it could
not have worked.

pop_within_returns_none_at_once: the Ok(None) IS the proof, with no clock.
It goes through the convenience, so the wait is SubmitWait, and SubmitIoRing
answers E_INVALIDARG when asked to wait with nothing pending. Remove the early
return and the call becomes an Err. Asserting Ok(None) therefore distinguishes
"returned early" from "waited" -- which is what the elapsed-time assertion was
being asked to do, and it cannot be wrong because the machine was busy.

the_deadline_is_honoured: that the loop WAITED is proved by the wait having
been called; that it STOPPED is proved by the test returning at all.

a_zero_bound_does_not_block: already asserted wait.calls == 0, which is the
causal statement of the same thing. The clock was pure redundancy.

a_wait_that_never_blocks: the elapsed < 5s assertion is deleted outright, not
replaced, because it could never have fired on the failure it named. If the
deadline were not honoured the loop would spin forever and that line would
never be reached. The only runs it could fail were slow ones, so it was
capable of false failures and incapable of true ones.

Censused first: five assertions in four unit tests, and none elsewhere in the
crate. The two remaining Instant uses in integration tests are a diagnostic
trace and a hang bound, neither an assertion about elapsed time.

Discriminating power verified unchanged. Removing the early return still turns
pop_within_returns_none_at_once red -- now via the Err path rather than the
clock -- and removing the 1ms clamp still turns the_wait_is_never_handed_a_zero
_timeout red.

Load-independence measured, not assumed: under 2x CPU saturation the unit suite
ran green 6 times with duration varying 30x, from 0.28s to 8.47s.

NOT established, and worth saying: an attempt to show the OLD assertion failing
under the same load was inconclusive. A first run reported 5/5 failures but the
message was not captured and was most likely a compile error from a bad patch;
a second, properly captured run passed. So the case for this change rests on
the structural argument above and on the retained sabotage coverage, not on a
demonstrated failure. That the old assertion is hard to provoke is precisely
why the original flake never reproduced.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…coverage

Exploratory. No decision recorded and nothing queued, because the proposal
would amend a decision that is currently written down and well argued.

The classification is not in doubt: the repository Quality rule lists an
operating-system API as an external boundary, so a test that opens a real ring
is an integration test wherever it lives. What M21.6 fixed was outcome
stability, not hermeticity -- removing five clock assertions left those tests
still opening a kernel ring. Conflating the two is what made the earlier answer
sound complete.

Measured: 63 of 131 lib tests open a real ring, and the split is clean -- four
modules are already fully hermetic. Of the 63, 25 use only public API and would
be a pure relocation; 38 also reach crate-private items, and what those 38 test
is bookkeeping rather than kernel behaviour.

Both feasibility unknowns came back clean. RingId::next is a process-global
AtomicU64 that never touches a handle, and IoRing's ten fields split 5/5, with
ring_id, next_user_data, outstanding, registered_files and registered_buffers
carrying no kernel state at all.

The proposal is one suite run against two backends, so the same assertions
check the fake and the kernel side by side. That is what has to survive the
crate's recorded rejection of a mock IoRing: the rejected thing is a mock used
INSTEAD of the kernel, where nothing checks the model against reality; this is
a mock used alongside, with the kernel run as the control case the crate
already demands of its spikes. The decision would be amended, not overridden,
and the write-up says plainly that if that distinction does not hold, the
proposal fails with it.

The bright line is explicit: the fake never gets a vote on Windows. Edge
triggering, one-waiter, drain ordering, the registration read, ERROR_TIMEOUT,
E_INVALIDARG and inline completion on a synchronous handle stay kernel-only --
which is precisely the set of findings this pass produced, and the argument for
drawing the line exactly there.

Three mechanics are costed, including the feature-gate consequence this
repository has already been bitten by, and four open questions are left for the
engineer rather than answered.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…edule M24

The engineer's verdict: the current design is incorrect and the only question
is when to fix it. So the part that does not depend on the open remedy is now
decided, and the work is scheduled rather than left as a proposal.

D-49 records the defect and the classification. 63 of 131 lib tests open a real
kernel ring, and the repository Quality rule already lists an operating-system
API as an external boundary -- so those are integration tests in the unit-test
location, and cargo test --lib does not mean what its name implies.

It also records the mis-diagnosis, because that is the part worth inheriting:
M21.6 removed five wall-clock assertions and reported that as the fix. It made
the outcomes load-independent -- 30x duration variation, zero outcome variation
under 2x CPU saturation -- and left every one of those tests opening a kernel
ring. Outcome-stable and hermetic are different properties.

What is NOT decided is the remedy. Three are costed in the design session and
the choice is gated on whether a fake whose assertions are shared with the
kernel escapes the crate's existing rejection of a mock IoRing. That rejection
stands until M24.1 concludes, and now carries a marker adjacent to its heading
saying so.

M24 has six items. M24.1 is the evaluation and gates M24.4; M24.2 and M24.3 are
safe under any outcome. M24.1 is required to settle the question BY
DEMONSTRATION rather than by argument -- build a fake with a deliberately wrong
accounting model and confirm the shared suite catches it, then give the fake a
wrong Windows belief and confirm the suite does NOT, which is the expected
result and the reason the bright line exists rather than being a hedge.

The bright line holds under every remedy: a fake never answers a question about
Windows. D-19, D-21, D-23, D-24, D-32, D-47, ERROR_TIMEOUT, E_INVALIDARG and
inline completion on a synchronous handle stay kernel-tested forever -- which is
exactly the set of findings that produced this crate's defects.

Sequencing is left open and the trade is stated: waiting compounds, because
every test-heavy milestone adds to the pile to migrate, and M22 is one.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…compound

M24 claimed the cost of waiting compounds "because every milestone that adds
tests adds to the pile to be migrated, and M22 is a testing-heavy milestone".
The mechanism is real; the instance was never checked, and it is false. All
three M22 items touch only examples/epoch_log/ and none adds a lib test.

Checked across the whole queue rather than for M22 alone, since wanting exactly
that check is what produced the error: no pending item outside M24 modifies
src/**/tests.rs. M23.1 is the SAMPLE's contract.rs, not the crate's -- a second
claim I made without checking in the same conversation. M20.1 and M20.6 are
documentation and the ring_copy sample; M23.2 is a decision that may imply API
later. The 63 do not grow while this waits.

So sequencing turns on other grounds, now stated: M24.2 is an internals refactor
of a published crate and the branch is already 19 commits with a feat and three
fixes on it; M24.1 could invalidate M24.4; and M22.1 unblocks M20.6, open since
2026-09-07.

The old wording is quoted in the correction rather than deleted, so the mistake
is legible to whoever reads the milestone next. This is the same defect as C-3
in the review session -- reasoning from a general principle to a specific case
without checking the case -- which is now twice in one conversation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…g items

Rule 6 was written around derived facts that look like facts -- versions,
release status, milestone counts. It did not reach the form that actually
caused the damage: a characterisation of another checklist item, arriving as a
subordinate clause inside an argument.

Both examples in the new clause are real and from one session. "M22 is a
testing-heavy milestone" and "M23.1 touches the crate's contract surface" were
both written into a milestone's sequencing rationale, and both were false when
checked: M22 is example-only in all three items, and M23.1 names the SAMPLE's
contract.rs rather than the crate's. The first reversed the milestone's
conclusion about when to schedule the work.

The clause names why this half is harder to apply. A version number announces
itself as a fact; "because Y is Z" reads as connective tissue while asserting
exactly as much. So it gives a syntactic tell -- the word "because" followed by
anything about another file, item or milestone -- rather than asking for more
diligence, which is what the rule already asked for and did not get.

No new section. The file has twenty-eight already, and its problem is not a
missing rule -- rule 6 and FAIL FAST rule 6 both covered this in spirit. The
problem is that they were stated as principles to remember rather than events
to trip on.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…on to it

Parked, not approved, no tooling. Recorded so both the idea and the engineer's
objection survive to a later conversation.

The objection is recorded as STANDING AND UNANSWERED: it is not clear this is
operationalizable to the degree claimed. Anyone picking it up should treat the
proposal as unproven and settle that first.

The durable part is the evidence, which stands whatever happens to the idea.
Eight process errors in one session, five of them claims about this repository's
own state that a command would have answered -- including two written into a
milestone's sequencing rationale, one of which reversed that milestone's
conclusion. The diagnosis: rules with a mechanical trigger fired reliably, rules
needing recognition that they applied did not, and the most carefully argued
sections in the instruction file are the ones breached.

Also records what was conceded when the proposal was made rather than extracted
afterwards -- chiefly that a script checks committed text while the triggers
that matter fire during generation -- and a kill criterion agreed in advance.

What actually landed is one clause on CONTRACT INTEGRITY rule 6, not this.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Mischaracterised in the note as a "standing objection, unanswered", which sets
the wrong bar for whoever reads it next: an objection blocks and must be
refuted before anything proceeds, while a concern marks the part that is
unproven and invites investigation. The engineer's position is the latter.

Also softened the section heading covering the narrowness point. That was
labelled an objection too, and it is better read as a reservation about one
approach -- it was raised and then worked around pragmatically in the same
breath, not pressed as a reason to stop.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…re the claim

Appender::append_batch and Lane::append_batch replace the single-record pushes.
Both compose as many records as there are free arena slots and submit once, so
the arena rather than the caller's list decides the batch -- which keeps the two
halves of an append honest, since a slot is composed into only while the kernel
is not reading it. With eight slots, an epoch of 64 records goes from 64
submissions to 8.

The teaching defect is the smaller half: a sample whose job is to teach Batch
was paying one SubmitIoRing per record, which is the one thing Batch exists to
avoid.

THE MEASUREMENT WAS THE POINT, AND IT RETURNED A NEGATIVE RESULT. Finding E-1
raised the possibility that the per-record cost was a term every strategy paid
equally, and so a shared constant capable of flattening the three-way
comparison into "indistinguishable" without that being true. Twenty runs, ten
each side, taken in one sitting by stashing the change so both sets came from
the same machine and build, are kept in
measurements/2026-09-22-append-batching/.

Throughput did not move distinguishably from noise: median records/sec shifted
1-5% while a single strategy's run-to-run range spans 1.17x to 1.57x. The
cross-strategy spread did not shrink and stayed at or below one strategy's own
range, which is the sample's own stated test. So E-1's hypothesis is not
supported and the existing conclusion survives a confound raised specifically
against it.

What did move is commit p50, the one figure here that separates: for
covering-flush the ten before-values and ten after-values barely overlap. That
is the expected shape -- batching removes seven of every eight submissions from
the append path, shortening the interval between the last append and the flush
being reached. Throughput is device-flush bound and does not move; latency is
not, and does.

Figures live in the capture and are LINKED from strategy.rs and M20.6 rather
than pasted into either, per the rule that a measurement has one home.

M21.3's property was preserved deliberately. Batching rewrites the append loop
and the naive version would have reintroduced a commit trigger reachable on a
pass that appended nothing; the loop continues when zero are accepted, so the
epoch check is reachable only after progress.

Unblocks M20.6, whose remaining question is the part no number speaks to.

Gate: fmt, clippy --all-targets --all-features, full test suite, and the example
end to end -- same epochs, same records, replay and negative control unchanged.

Completed item: M22.1: Batch an epoch's appends into one submission in both
append paths, and measure whether the per-record submission cost was flattening
the strategy comparison.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…king them

The sample had two answers to "which arena slots are free". Appender asked the
arena, filtering on outstanding(slot) == Some(0). Lane kept a Vec<u32> and
maintained it by hand. Both now call one free_slots in append.rs; Lane's field,
its initialiser, and its push-on-claim are gone.

THE ITEM'S PREMISE WAS WRONG IN THE DIRECTION THAT MATTERED. It said "both are
correct and the cost difference is nil ... the duplication is the defect,
because the two CAN drift". They had already drifted. The free list took its
slot BEFORE composing into it, so an append refused between the two -- a record
too long for a slot is the reachable path -- returned with the slot popped and
no operation ever issued. The arena considered that slot quiet forever; the
list never offered it again. SLOTS such refusals and the harness reports a full
arena while the kernel holds nothing. Typed fix rather than refactor for that
reason: deriving the fact removed a live bug, and removed it by construction --
a slot nothing was pushed against never stopped being free.

Verified by sabotage, in both directions. Breaking free_slots to offer busy
slots failed the Lane path with the arena's own refusal, while the appender ran
clean -- so that run proved only half of the "both callers bind" claim. A second
sabotage (take(0)) starved the appender, which then failed before the strategy
section was reached. And the leak itself was measured rather than argued:
re-injecting the free list and failing eight appends left the lane reporting 0
of 8 slots free with the arena entirely idle.

The new strategy/tests.rs says plainly what it does NOT catch. It asserts the
property from the arena's side, so a re-introduced free list would not fail it
-- under the re-injection above it passed, and only a temporary assertion
against the list itself went red. What keeps a second definition from returning
is that there is one function and both callers call it. Claiming the test
covers that would be the cosmetic binding this repository's rules warn about.

Two harness defects are recorded in the archive entry because both produce a
FALSE GREEN: a .Replace that matched nothing reported success and ran an
unmodified tree, and a PowerShell helper that logged with Write-Output returned
its log line into the patched text. Per-site match-count assertions caught the
first.

Queues M22+.2 rather than deciding it silently: outstanding()'s own rustdoc
names this use case and both in-repo consumers hand-rolled it anyway, so the
primitive may belong on RegisteredBuffers. Deferred with the blocker named -- a
public API addition needs a lib test, and that test opens a real ring, which is
the pile M24 exists to drain.

Gate: fmt, clippy --all-targets --all-features, full suite including doctests,
and the example end to end -- same epochs, same records, replay and negative
control unchanged.

Completed item: M22.2: Collapse the two free-slot implementations to one.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…erately

M22.3 offered a choice: adopt ring_copy's allocator in the epoch-log sample,
or write down why a durability sample makes no locality decision. Both halves
were needed, and a third thing fell out of doing them.

THE PLACEMENT DECISION. examples/epoch_log/placement.rs asks the log's own
handle through FSCTL_QUERY_VOLUME_NUMA_INFO -- the documented call this crate's
design notes already established, which takes a file or directory handle
directly and needs no device-tree walk -- and the arena is allocated preferring
whatever comes back. A volume naming no node yields no preference and the log
runs on.

NO BENEFIT IS CLAIMED, AND THE SAMPLE SAYS SO IN ITS OWN OUTPUT. The arena is
eight slots of four kilobytes against a workload bound by a per-epoch device
flush costing hundreds of microseconds, which M22.1 measured directly. This
demonstrates how the decision is made and reported, not that it was worth
making; ring_copy remains where placement meets a load that could show it. The
report line is qualified by GetNumaHighestNodeNumber, because on a one-node
machine "placed on node 0" is true and misleading -- it instead says the choice
was never available. Two tests hold that in BOTH directions: the disclaimer
must appear for a single-node machine and must NOT appear for a multi-node one,
since a disclaimer that shows up everywhere trains a reader to ignore it.

NUMABUFFER MOVES INTO THE LIBRARY (D-51). This is additive and NOT breaking:
no existing item changed meaning, so the subject carries no bang and this earns
a minor bump rather than a major one. The front page named this allocation as the highest-leverage
locality decision available and then supplied nothing, so the first consumer
wrote it in a sample and this item was about to write the second. It decides no
policy: which node stays the caller's answer per D-8, and the type records that
nndPreferred is a preference, so a successful allocation is not evidence the
pages landed there.

THAT MOVE SURFACED A PACKAGING DEFECT THAT WOULD HAVE REACHED A CONSUMER.
`cargo check --all-targets` unifies dev-dependency features into the build, so
the library's newly-required Win32_System_Memory was being supplied by the dev
list and the lib target compiled clean. `cargo doc` -- which gets no
dev-dependencies -- failed immediately, and `cargo check --lib` confirmed it:
anyone depending on this crate alone would not have compiled it. The manifest
now carries the feature on the library dependency, and the comment asserting
"the library itself needs none of them" is corrected rather than left to
mislead.

Verified by sabotage, in both directions. Forcing the FSCTL to report a node the
machine does not have failed the run at arena allocation with
ERROR_INVALID_PARAMETER, proving the queried node reaches the allocator rather
than being reported decoratively. Forcing the FSCTL to fail produced the
Unplaced line, its stated reason, and a log that still kept its contract -- the
path that does not otherwise execute where the query succeeds.

Swept the claim rather than the cited site: README.md, lib.rs, two DESIGN-NOTES
sections and the manifest comment were updated; design-session and archive
copies were left as historical record. M23.2 was narrowed in the same pass,
because its option (a) is no longer "should a sample do this".

Provenance: the move left the files 47% similar, under git's rename threshold,
so a header comment and the trailer below record what blame cannot.

Gate: fmt, clippy, full suite including doctests, cargo doc, lib-only and
--no-default-features builds, borrow-surface/encoding/publishable checks,
default workspace debug and release, and both examples end to end (ring_copy's
copy verified by hash).

Completed item: M22.3: Give the registered arena a stated placement, or state
why it has none.

Moved-From: crates/windows-ioring-sys/examples/ring_copy/buffer.rs
Moved-To: crates/windows-ioring-sys/src/numa_buffer.rs
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…th the wrong evidence

M22+.2 proposed RegisteredBuffers::quiet() and cited M22.2's slot leak as the
reason for it. Checked against 7c12708: the leak was `self.free.pop()` --
the hand-maintained TRACKING, before composing -- not the free-slot query. The
arena's outstanding counts were never involved, so the proposed API would not
have prevented the bug that justified it.

Two further facts point the same way. outstanding() already carries that
affordance and has said so in its own rustdoc since before M22.2 ("lets a
caller pick a quiet buffer ... the normal shape for an arena cycling through
its slots"), so the item's premise -- a crate that documents a pattern and
makes every caller reimplement it -- was already false. And both consumers had
it available and hand-rolled a free list regardless, so a second, tidier
spelling has no reason to change behaviour the first spelling did not.

ITS STATED BLOCKER WAS ALSO FALSE, which is worth recording separately because
it is the more embarrassing half. The item deferred itself on "it needs a lib
test, and a RegisteredBuffers can only be obtained from a real registration --
so that test opens a kernel ring". src/batch/tests.rs already contains five
lib tests that open a ring and register buffers. M22.3 then added public API
with fourteen new lib tests on this same branch an hour later. The objection
was both untrue and applied inconsistently.

The replacement asks the question the evidence actually supports, and rests on
a census rather than recollection: NINE sites keep a map from UserData to an
unclaimed Token and claim it when the completion is popped -- six tests, three
in the epoch-log sample. Two of them independently declare a `type Pending`
alias carrying the SAME doc comment. Two of the nine add registered slots on
top, and those two are the slot arena. So there are two questions, not one:
whether the pending map is a library type, and separately whether a slot arena
follows.

M23.3 carries the counter-argument rather than only the case for: six of the
nine sites are tests, test convenience is a weak reason to grow permanent
public surface, and the honest contrast is NumaBuffer (D-51) -- ~90 lines of
unsafe FFI and RAII a caller cannot obtain otherwise, against a HashMap a
caller writes in three lines. "Neither" and "a test-util module" are live
answers.

M22+.2 is deleted rather than left unchecked: a superseded item is not pending
work, and parking it would have implied M22+ was incomplete. Its lesson --
do not re-propose quiet() without new evidence -- moved into M23.3, where
whoever works this area will read it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ot commits

M20.6 asked whether AlternatingRings still earns its cost, and whether the
published numbers should be re-read, re-run or annotated. Investigating it
found the numbers do not measure what they are labelled, so none of those three
helps.

THE HARNESS NEVER WAITS FOR A FLUSH. Decomposing the reported commit latency
into deferral (flush pushed -> harness next looked) and blocking (time actually
waiting) gave blocking p50 AND p99 of 0 us for all three strategies. The
published commit p50/p99/max column is entirely deferral: it reports how long
the next epoch's appends took.

That also explains AlternatingRings' apparently-worse latency. It settles a
flush on a two-epoch rotation against everyone else's one, so its window is
twice as wide. The 3-4x penalty in the published table is an artifact of the
deferral schedule, not a property of the strategy.

THERE IS NO PIPELINE UNDERNEATH ANY OF IT. The commit's SubmitIoRing took
289-555 us and returned with all 9 completions already queued: the handle has no
FILE_FLAG_OVERLAPPED, so the batch ran inline and the flush was paid inside
submit. The comment reading "a real log keeps appending while a commit is
outstanding" describes something that cannot happen there.

ALTERNATINGRINGS IS ANSWERED STRUCTURALLY, AND NEEDS NO MEASUREMENT.
RegisteredBuffers::get_mut refuses a slot with an operation outstanding and
there are SLOTS slots, so at most SLOTS appends are outstanding on a ring by
construction -- and each alternating lane registers its own arena of the same
size. The per-ring bound is identical either way. Probing agreed (8 and 8), but
the argument does not rest on that and holds whatever the platform does about
pending. S-2's "unbounded in unrelated traffic" does not apply here: the arena
bounds it, not the ring topology.

The new spike establishes which configurations pend at all, and its history is
the more useful half. Its first draft ran each condition ONCE and printed a
verdict -- the exact error D-47 records, where a handful of runs turned an
observation into a written-down guarantee. Rewritten to 500 trials per
condition, it immediately justified itself: FILE_FLAG_OVERLAPPED alone changed
nothing (0/500), only NO_BUFFERING over a pre-written extent pended reliably
(500/500, submit p50 falling ~500 -> 116 us), and the NO_BUFFERING-extending
condition reported 5/500 in one run and 271/500 in the next, minutes apart and
unchanged. A single run would have called that "never".

NONE OF IT IS A CONTRACT, INCLUDING THE STABLE ROWS. Windows specifies nothing
about when a ring operation completes relative to SubmitIoRing. A rate of zero
bounds a frequency; a rate of 500/500 is the same statement pointing the other
way -- it may never have been false here and is still not contractually true.
Both the spike and M25 carry that as a standing constraint, because the obvious
use of the spike is the wrong one: picking the flags that pended and rebuilding
on them would bind the sample's premise to incidental behaviour.

M25 is the plan that follows -- sector stride, replay over the stride, a
pre-allocated extent opened NO_BUFFERING|OVERLAPPED, a real commit measurement,
the re-run, and the sweep. The sample may ADOPT that shape, which is what real
write-ahead logs do; no item in it may DEPEND on an operation pending.

M20.6 keeps the half that is answered and is blocked on M25 for the rest.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The whole-machine fallback in Policy::select is the branch every zero-relation
machine takes -- the shape D-48 records as ordinary rather than exotic -- and it
cannot be reached by RUNNING the sample on a machine that reports its
relations. A synthetic topology reaches it. Fifteen tests.

THE ITEM'S REASON FOR DEMANDING BOTH HALVES WAS VERIFIED, NOT TRUSTED. It
argued that a test of the absent case alone "would pass against a function that
always degrades". Sabotaging select to degrade unconditionally showed exactly
that: a_policy_whose_relation_is_absent_... STILL PASSED, while four
present-case tests failed. The reverse sabotage -- never degrade -- failed five
absent-case tests. Neither half is redundant, and that is now measured rather
than argued.

One test, degrading_unconditionally_would_fail_a_test_here, puts that
dependency in code rather than in a comment, so a future edit that removes the
present-case coverage has something named to remove.

DONE WITHOUT WAITING ON SH-4.12, AND THE COUPLING WAS NARROWED RATHER THAN
IGNORED. The recorded callout said that item "rewrites the selection arm this
test would assert against". True only of a test asserting through ByL3. The
fallback tail is shared by all five policies and is not what SH-4.12 changes --
it changes which domains ByL3 matches -- so exercising it through ByNode and
ByPackage pins nothing. Both checklists now record the narrower coupling, and
SH-4.12 inherited the one assertion genuinely its own: ByL3's degradation
condition, under whichever rule replaces the level: 3 match.

Beyond the two halves the cases cover what the fallback must get right and what
it must not claim: a memory domain with no processors is not a usable node;
degradation is per-policy, not a property of the machine; Single returns the
whole machine UNDEGRADED, because degrading is a statement about not getting
what was asked for and Single asked for exactly this; the fallback covers every
online processor, excludes reserved-but-offline slots, and spans processor
groups; and it carries no observations, because nothing observed it.

The synthetic memory domain uses Observed::NotObserved for its size rather than
Known(0) -- the type's own docs call that the variant "a hand-written
description leaves behind". Known(0) would assert a measurement nobody made, in
a test whose subject is honest reporting.

ring_copy was auto-discovered and therefore NOT a test target, so cargo test
would have compiled these and run nothing. It now has an explicit [[example]]
entry with test = true, for the same reason epoch_log has one.

Gate: fmt, clippy --all-targets --all-features, full suite including doctests.

Completed item: M20.3: Make ring_copy's degraded-fallback path observable in a
test.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ter on L3

M20.1 and SH-4.12 land together because they are one change. M20.1's sweep
reaches policy.rs's doc comments, and rewriting those to describe the new rule
while the code still filtered `level: 3` is exactly the contradiction the
blast-radius convention exists to prevent. Splitting them would have produced a
commit whose documentation lied.

THE ITEM'S EVIDENCE WAS A SHIPPING ARM PART WITH NO L3. MEASURING THE CONSUMER
FOUND A SECOND SHAPE, ON THIS WORKSPACE'S OWN MACHINE, THAT NEITHER ITEM
ANTICIPATED. It reports an L3 spanning all 16 processors above a real 8-way L2
partition, so the old filter did not fail the way the item assumed:

  old `level: 3` filter        -> 1 domain, mask 0xffff, degraded = NO
  outermost_partitioning_cache -> 8 domains at L2, degraded = no

The old code MATCHED SOMETHING, so it did not degrade. It reported success
while collapsing an eight-domain machine to a single ring -- a silent wrong
answer rather than a visible fallback, and live on the machine this repository
is developed on rather than on hardware nobody here owns.

Three consumers restated the rule, not the two the items named. policy.rs and
the prose were known; examples/l3_domains.rs also hardcoded `cache.level == 3`
and was named after the assumption. It is now examples/cache_domains.rs, asks
the same primitive, and reports the level it found. The rewrite left it 17%
similar, under any useful rename threshold, so a provenance header records what
blame cannot.

BYL3 AND L3 ARE REJECTED RATHER THAN ALIASED, which is the breaking part and
why the subject carries a bang. They named a rule the sample no longer
implements; mapping them onto ByCache would let a script keep asking for L3 and
keep believing it got L3 -- on an L2-partitioned machine, a wrong answer
delivered quietly. An unknown policy prints the usage line, which is a question
rather than a wrong answer.

Five tests were added to the file M20.3 created two hours earlier -- the
assertion SH-4.12 had inherited from it. Re-injecting the level: 3 filter fails
three of them, including the one pinning the measured shape above.

Swept: DESIGN-NOTES.md (the heuristic section, the sizing note, the policy list,
D-27's pointer, and D-48's own "restating the rule is M20.1" reference, which
was itself a restatement that would have gone stale), README.md, src/lib.rs,
both examples, and both checklists.

Gate: fmt, clippy, full suite, both examples end to end with ring_copy's copy
verified by hash, workspace debug and release.

Completed item: M20.1: Correct the L3 heuristic's justification and sweep every
restatement.
Completed item: SH-4.12: ring_copy's ByL3 policy restates the partition rule
instead of asking for it.

Renamed-From: crates/windows-ioring-sys/examples/l3_domains.rs
Renamed-To: crates/windows-ioring-sys/examples/cache_domains.rs
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Mike Grier and others added 6 commits September 25, 2026 14:50
… requested

M26.10 asked whether a short transfer is possible. It is, and the answer has
two halves belonging to different layers -- collapsing them is what the
question had been deferred over.

The general ring permits it. WriteFile documents that "when writing to a
non-blocking, byte-mode pipe handle with insufficient buffer space, WriteFile
returns TRUE with *lpNumberOfBytesWritten < nNumberOfBytesToWrite"; sockets
report a short send against a full transmit buffer, and a timed comm handle a
partial one. So the clause is Documented rather than Observed. The permission
cannot be narrowed not because files behave badly -- a successful completion on
an ordinary file carries the full length, and a full volume is ERROR_DISK_FULL
rather than a short success -- but because IoRing takes a handle and never asks
what kind it is.

Continuation stays with the caller per D-67: the count is reported and nothing
reissues the remainder. A short count may be zero, so a consumer that loops
must tolerate making no progress.

The resolver can now produce one (may_transfer_partially, default on), and the
RS-P-8 test checks both directions: with the switch on some seed reports short,
with it off none may, and no count ever exceeds its request. Two sabotage cases
verified caught: suppressing short counts entirely, and over-delivering.

Three kernel assertions that required a full count bare now state that
completeness is a property of the temp file they opened -- the form
flush_barrier.rs already used and M26.7 singled out as right.

Swept the permission count: the resolver config guard caught its own
restatement, and D-59 enumerated the clauses inline; both updated. The archive
was left alone.

Spawns M26.11: examples/epoch_log must constrain its handle types. Its
guarantees are not portable across the range IoRing accepts -- a flush barrier
means nothing on a socket -- so it must earn the completeness its epoch
accounting assumes rather than inherit it.

Completed item: M26.10: Decide whether a completion may report fewer bytes than
requested, which RESPONSE-SPACE.md currently lists as deliberately undecided.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
GetFileType, accepting only FILE_TYPE_DISK: one documented call whose table
lands on the classes RS-P-8 cites, with FILE_TYPE_PIPE covering "a socket, a
named pipe, or an anonymous pipe" in a single value.

Also records what it does not establish, beside the check rather than in a
design note: FILE_TYPE_REMOTE is documented Unused and SMB files report
FILE_TYPE_DISK, so local-vs-remote needs FileRemoteProtocolInfo; and write-cache
or FUA behaviour is a device property, so a FILE_TYPE_DISK handle on an
unprotected volume passes the type check and still cannot back the claim.

FILE_TYPE_UNKNOWN is both a valid answer and the failure indicator, so the last
error must be cleared before the call and read after it.

Prefers the build rung: RawHandle is threaded through four sites today, and a
newtype with a checked constructor makes a socket unrepresentable rather than
merely detected.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… gate

The item asserted that an SMB file reports FILE_TYPE_DISK. That was
recollection rather than a citation, and it is withdrawn -- the same failure
M26.8 corrected. Nothing public states how a network path classifies, and
GetDriveType's page notes that SMB does not support volume management
functions, so the question is open and must be answered by probing.

Reframes FILE_TYPE_DISK as a type gate rather than a durability gate: the
documented table gives three buckets and does not enumerate which device
classes land in each, so it establishes only "not a pipe, socket, or character
device" and must not be read as "a fixed local volume".

Adds the second probe the claim actually turns on -- GetDriveType separates
FIXED, REMOTE, CDROM, RAMDISK and REMOVABLE by name -- against the handle-based
FileRemoteProtocolInfo, which answers only remoteness but needs no path round
trip. Notes that refusing DRIVE_RAMDISK should be uncontroversial.

Also records that a successful FILE_TYPE_UNKNOWN is a real response rather than
a near-failure, and that no check here establishes that a flush reaches stable
media, so the contract must not imply it has been checked.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
D-69 left the durability layer owing a handle constraint, and the obvious
discharge was a gate: GetFileType, then GetDriveType or FileRemoteProtocolInfo
for the distinctions the first is too coarse to make. Working the gate out is
what showed it was not worth having.

Sorted by how they present, the failures fall either side of what a check can
see. A console handle, pipe, socket or closed handle is what GetFileType names,
and each already fails at the first positioned write -- the check buys a message.
A RAM disk, a remote share, or a volume whose write cache is not power-protected
succeeds at every API call this log makes and silently fails to be durable, and
nothing in Win32 settles the last from a handle at all, because write-cache
state belongs to the device rather than the handle.

So the gate guards the loud cases and misses the silent ones, which is the
inverse of what a durability layer needs -- and its cost is not the call but the
claim, since a check that cannot establish the property still reads as though it
had.

The requirement is therefore documented and the caller warrants it: D-67's shape
applied to handles rather than retries. Two alternatives are recorded as
declined so they are not re-proposed -- refusing FILE_TYPE_CHAR/UNKNOWN, and a
caller-declared type mask whose value would be acknowledgment rather than
validation.

M26.11 is rewritten accordingly: write the contract, single out the transfer
requirement as checkable and the durability requirement as a warranty, and
guard only what is guardable.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…he checkable one

M26.11. The log is handed a handle and never opens one, so its durability
guarantees rest on that handle sustaining what the program issues. Those
requirements were stated nowhere.

contract.rs gains a fourth clause, Requires, beside Guarantees,
DoesNotGuarantee and Assumes. It is distinct from Assumes by who can act on
it: a requirement is something the caller chooses -- how the handle is opened,
what it refers to -- while an assumption is about the world and the only
response is different hardware. Merging them would bury the actionable in the
unverifiable. This is the growth Clause::ALL's own doc anticipated, and the
exhaustive heading() match is what forced the edit.

Two of the six requirements are different in kind. That a successful write of
N bytes transferred N is checkable, and is now checked: append.rs bound the
count to `_written` and discarded it, so the requirement was enforced nowhere.
That a completed flush reached stable media is a warranty the caller gives,
stated in those words, because a contract that merely sounds confident about
durability is how a silent failure gets built on.

No real handle this sample opens produces a short write, so the new error edge
was unreachable by any test. Completion::with_injected_transfer is the seam
that reaches it -- the counterpart to with_injected_failure for a *successful*
short count, which a failure cannot model. Unlike the failure seam it is inert
everywhere, since only the byte count moves and no path keys memory ownership
off it. The test asserts both directions: the same injection accepted at the
full stride and rejected one byte short.

Sabotage: suppressing the comparison is caught. The sweep also found two cases
gone stale on the reshaped ALL array and they are repaired; all 53 now behave
as declared.

Records the no-gate reasoning at the contract itself, per D-70, so it is not
reproposed: a pre-flight check catches the failures that were already loud and
misses every failure that is silent.

M26 is complete, so its milestone framing migrates to COMPLETED-CHECKLIST.md
and its per-item stubs are dropped.

Completed item: M26.11: Document the capability requirements examples/epoch_log
places on the handle it is given, and let an unmet one surface at the operation
that needs it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The crate was added to release-please-config.json when it was created but never
to .release-please-manifest.json -- 15 configured packages against 14 manifest
entries, and no release tag.

This is load-bearing rather than tidiness. windows-ioring-sys depends on
win-numa-sys = { version = "0.1.0", path = ... }, a versioned path dependency,
and win-numa-sys does not exist on crates.io. A windows-ioring-sys release that
did not also publish win-numa-sys would fail at cargo publish. The publish
workflow already waits for workspace siblings to appear on the index, so the
ordering is handled once both tags exist; what was missing is release-please
producing the sibling release at all.

0.0.0 rather than 0.1.0 is deliberate: the manifest records the last *released*
version, and nothing has been released. From 0.0.0, the breaking commit that
created the crate bumps to 0.1.0 under bump-minor-pre-major, matching the
version already in its Cargo.toml. Recording 0.1.0 would instead assert that
version had shipped and bump past it, leaving 0.1.0 permanently unpublished.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI lite review requested due to automatic review settings September 25, 2026 20:41
The crate had its tag trigger but was missing from the workflow_dispatch
choices and from the workspace_crates registry, which the publish check named
exactly: "not a workflow_dispatch choice, so it cannot be published by hand
either", and "absent from workspace_crates, so a dependent can race its tag
instead of waiting for it".

The second is the one that matters here. windows-ioring-sys carries a versioned
path dependency on win-numa-sys, which is not yet on crates.io, so the sibling
wait is what keeps its publish from running before the dependency is on the
index. Absent from that registry there is nothing to wait for, and the two tags
push together.

Found by CI rather than by reading: the guard failed on the release PR while
the manifest bootstrap in 29a6ebd looked complete on its own.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Unresolved critical and moderate findings remain in event delivery, cross-platform tracing, release publishing/versioning, and benchmark fidelity.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 2 High severity · 2 Medium severity · 1 Low severity

Open (5)
What changed in this PR

This PR delivers the M20–M26 windows-ioring-sys work, adds win-numa-sys, improves durability and response-space testing, and fixes IoRing event delivery.

Changes:

  • Adds NUMA APIs and migrates IoRing consumers.
  • Adds bounded waits, durability measurements, resolver testing, and cache-policy updates.
  • Updates tracing, documentation, CI, and release automation.
File Reviewed change / final note
release-please-config.json Registers win-numa-sys.
DESIGN-RATIONALE.md Documents design rationale.
crates/​windows-threadpool-sys/​src/​trace/​tests.rs Adds trace tests; coverage and retention behavior remain under review.
crates/​windows-threadpool-sys/​src/​lib.rs Exposes tracing; the trace feature must remain portable on non-Windows targets.
crates/​windows-threadpool-sys/​Cargo.toml Adds the trace feature.
crates/​windows-ioring-sys/​tests/​ring_lifecycle.rs Adds lifecycle coverage.
crates/​windows-ioring-sys/​tests/​kernel_span.rs Bounds completion waits.
crates/​windows-ioring-sys/​tests/​generated_sequences.rs Bounds generated waits.
crates/​windows-ioring-sys/​tests/​fault_injection.rs Uses bounded completion retrieval.
crates/​windows-ioring-sys/​tests/​failure_paths.rs Bounds failure-path waits.
crates/​windows-ioring-sys/​tests/​completion_event.rs Bounds event completion retrieval.
crates/​windows-ioring-sys/​src/​token.rs Separates token accounting.
crates/​windows-ioring-sys/​src/​numa_buffer_io/​tests.rs Tests NUMA buffer trait wiring.
crates/​windows-ioring-sys/​src/​numa_buffer_io.rs Implements IoRing buffer traits.
crates/​windows-ioring-sys/​src/​event_delivery/​tests.rs Updates event-delivery tests.
crates/​windows-ioring-sys/​src/​event_delivery.rs Fixes arm-before-signal ordering; already-attached events still need a setup signal for backlog delivery.
crates/​windows-ioring-sys/​RING-OPENING-LIB-TESTS.txt Adds generated test inventory.
crates/​windows-ioring-sys/​README.md Updates topology and buffer guidance.
crates/​windows-ioring-sys/​measurements/​2026-09-24-set-len-zero-fill-cost/​runs.txt Records allocation measurements.
crates/​windows-ioring-sys/​measurements/​2026-09-24-set-len-zero-fill-cost/​prealloc-cost.rs Adds the benchmark; its chunk size, source header, and output sink need alignment with repository conventions.
crates/​windows-ioring-sys/​measurements/​2026-09-24-set-len-zero-fill-cost/​at-sample-sizes.txt Records sample-size measurements.
crates/​windows-ioring-sys/​measurements/​2026-09-24-set-len-vs-zero-fill/​runs.tsv Records allocation comparisons.
crates/​windows-ioring-sys/​measurements/​2026-09-24-set-len-vs-zero-fill/​runs-with-touch-end.tsv Records touched-end comparisons.
crates/​windows-ioring-sys/​measurements/​2026-09-24-allocation-knee/​runs.txt Records allocation-knee measurements.
crates/​windows-ioring-sys/​measurements/​2026-09-22-append-batching/​before/​before-9.txt Records pre-change batching data.
crates/​windows-ioring-sys/​measurements/​2026-09-22-append-batching/​before/​before-8.txt Records pre-change batching data.
crates/​windows-ioring-sys/​measurements/​2026-09-22-append-batching/​before/​before-7.txt Records pre-change batching data.
crates/​windows-ioring-sys/​measurements/​2026-09-22-append-batching/​before/​before-6.txt Records pre-change batching data.
crates/​windows-ioring-sys/​measurements/​2026-09-22-append-batching/​before/​before-5.txt Records pre-change batching data.
crates/​windows-ioring-sys/​measurements/​2026-09-22-append-batching/​before/​before-4.txt Records pre-change batching data.
crates/​windows-ioring-sys/​measurements/​2026-09-22-append-batching/​before/​before-3.txt Records pre-change batching data.
crates/​windows-ioring-sys/​measurements/​2026-09-22-append-batching/​before/​before-2.txt Records pre-change batching data.
crates/​windows-ioring-sys/​measurements/​2026-09-22-append-batching/​before/​before-10.txt Records pre-change batching data.
crates/​windows-ioring-sys/​measurements/​2026-09-22-append-batching/​before/​before-1.txt Records pre-change batching data.
crates/​windows-ioring-sys/​measurements/​2026-09-22-append-batching/​after/​after-9.txt Records post-change batching data.
crates/​windows-ioring-sys/​measurements/​2026-09-22-append-batching/​after/​after-8.txt Records post-change batching data.
crates/​windows-ioring-sys/​measurements/​2026-09-22-append-batching/​after/​after-7.txt Records post-change batching data.
crates/​windows-ioring-sys/​measurements/​2026-09-22-append-batching/​after/​after-6.txt Records post-change batching data.
crates/​windows-ioring-sys/​measurements/​2026-09-22-append-batching/​after/​after-5.txt Records post-change batching data.
crates/​windows-ioring-sys/​measurements/​2026-09-22-append-batching/​after/​after-4.txt Records post-change batching data.
crates/​windows-ioring-sys/​measurements/​2026-09-22-append-batching/​after/​after-3.txt Records post-change batching data.
crates/​windows-ioring-sys/​measurements/​2026-09-22-append-batching/​after/​after-2.txt Records post-change batching data.
crates/​windows-ioring-sys/​measurements/​2026-09-22-append-batching/​after/​after-10.txt Records post-change batching data.
crates/​windows-ioring-sys/​measurements/​2026-09-22-append-batching/​after/​after-1.txt Records post-change batching data.
crates/​windows-ioring-sys/​examples/​ring_copy/​policy.rs Uses cache-partition policy.
crates/​windows-ioring-sys/​examples/​ring_copy/​plan.rs Uses NumaNode.
crates/​windows-ioring-sys/​examples/​ring_copy/​main.rs Updates policy defaults.
crates/​windows-ioring-sys/​examples/​ring_copy/​engine.rs Uses shared NUMA buffers.
crates/​windows-ioring-sys/​examples/​ring_copy/​buffer.rs Removes the relocated allocator.
crates/​windows-ioring-sys/​examples/​l3_domains.rs Removes the obsolete L3 example.
crates/​windows-ioring-sys/​examples/​epoch_log/​tests.rs Adds durability coverage.
crates/​windows-ioring-sys/​examples/​epoch_log/​replay.rs Adds stride-aware replay.
crates/​windows-ioring-sys/​examples/​epoch_log/​record/​tests.rs Tests digest behavior.
crates/​windows-ioring-sys/​examples/​epoch_log/​commit.rs Documents failed commits.
crates/​windows-ioring-sys/​examples/​epoch_log/​checkpoint.rs Documents ring/log separation.
crates/​windows-ioring-sys/​design-sessions/​spikes/​file-handle-numa-spike.rs Corrects spike rationale.
crates/​windows-ioring-sys/​design-sessions/​DESIGN-SESSION-2026-08-28-external-consumer-correspondence.md Records design corrections.
crates/​windows-ioring-sys/​BORROW-SURFACE.txt Updates the borrow inventory.
crates/​win-numa-sys/​src/​lib.rs Adds NUMA APIs.
crates/​win-numa-sys/​README.md Documents the NUMA crate.
crates/​win-numa-sys/​PLANS.md Records crate plans.
crates/​win-numa-sys/​DESIGN-NOTES.md Records design notes.
crates/​win-numa-sys/​CHECKLIST.md Tracks crate work.
crates/​win-numa-sys/​Cargo.toml Defines the new crate.
crates/​topology-planner/​PLANS.md Updates planner plans.
crates/​topology-planner/​DESIGN-RATIONALE.md Updates planner rationale.
crates/​topology-planner/​COMPONENT.md Updates component documentation.
CHECKLIST.md Records milestone work.
CHECKLIST-ship-topology-and-queues.md Records cache-policy completion.
CHECKLIST-io-domains.md Records durability handoff.
Cargo.toml Adds the workspace crate.
Cargo.lock Locks the new dependency.
.release-please-manifest.json Seeds release metadata; the seed version must produce the intended 0.1.0.
.github/​workflows/​publish-crate.yml Adds tag publishing; manual choices and sibling-index waiting must include win-numa-sys.
.github/​workflows/​ci.yml Adds validation jobs.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread crates/windows-ioring-sys/src/event_delivery.rs
Comment thread crates/windows-threadpool-sys/src/lib.rs
Comment thread .github/workflows/publish-crate.yml
Copilot AI review requested due to automatic review settings September 25, 2026 20:48

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

One or more issues must be addressed before approval.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 3 High severity · 1 Medium severity · 1 Low severity

Open (5)
Resolved since last review (1)
Previously missed (2)

In code that hasn't changed since last review

Medium severity Do not mark work complete while ByCache test is missing

CHECKLIST-ship-topology-and-queues.md:695

This item is checked off, but the completion note explicitly says it still owes ByCache's degradation test. That leaves scheduled work marked done; either add the missing verification before checking the item, or move the follow-up to a later milestone and keep this item limited to the completed change.

Low severity Route measurement output through a single sink

crates/​windows-ioring-sys/​measurements/​2026-09-24-set-len-zero-fill-cost/​prealloc-cost.rs:41

This new measurement executable writes directly through several println! call sites. The repository's output rule requires a single writer/sink abstraction so formatting is separable from the destination; route the table and summary through one sink rather than adding another multi-site stdout tool.

Comment thread crates/windows-ioring-sys/tests/ring_lifecycle.rs
CI runs rustdoc with broken and private intra-doc links denied, in two
configurations this branch had stopped satisfying. None of the six links exist
on main, so all were introduced here.

Under --no-default-features the items behind `threadpool` and `kernel-seam` are
not compiled, so links to EventDelivery, RingScope, Responses and sys::install
had nothing to resolve against. Two fixes, split by which way the feature
defaults. `threadpool` is on by default, so those links are kept for the
configuration nearly every reader sees and degrade to a code span via cfg_attr
when it is off. `kernel-seam` is off by default and each of those sentences
already says "with the feature on", so a code span there loses the reader
nothing.

Under --workspace --all-features, two more: trace.rs referenced `enabled`
unqualified, which does not resolve from module scope even though the re-export
is unconditional, and is now fully qualified; and resolver.rs linked
super::installed from public documentation, which is a private module, so it
becomes a code span naming the module instead.

Also drops a stray blank line in completion_event's rustdoc.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings September 25, 2026 20:57

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Unresolved moderate findings affect event delivery, platform gating, and tracing correctness and performance.

Review effort: Lite
Findings: 3 High severity · 1 Medium severity · 1 Low severity

Open (5)

…vered

Four of five Copilot findings applied; one declined with evidence; the first
opened an investigation that is queued rather than patched.

trace was the only Win32-backed module in windows-threadpool-sys left ungated,
and its traced build calls GetCurrentThreadId, so --features trace on a
non-Windows target broke the crate's empty-on-other-targets behaviour. Now
cfg(windows) like its siblings.

The prealloc benchmark filled in 1 MiB chunks where the sample uses 64 KiB, so
it did not measure the chunking of the code it reports on. The constant now
matches, the data is regenerated, and the README figures are updated with it --
including a note that the re-run also happened on a busy machine, so the two
captures differ by more than the chunk size and are not a controlled
comparison. It also lacked the required copyright header.

Declined: gating tests/ring_lifecycle.rs on cfg(windows). The finding argues it
is unlike the other real-ring targets, and 15 of this crate's 17 test targets
are ungated, so it matches the convention rather than departing from it. Making
all of them consistent is a 15-file sweep and a separate decision.

The remaining finding -- that EventDelivery::new signals only when it attached
the event itself -- is real, and the repair it suggests does not work. A
reproducer that attaches the event, consumes the signal attaching raised,
queues work, and consumes the completion signal fails 6 of 6 whether the signal
is conditional or unconditional. What makes it pass is a 50 ms sleep between
wait.arm and the signal, 3 of 3, as does --features trace.

So the wakeup is lost in a window after arming rather than never raised, which
is wider than the report and bears on D-68: M26.9 fixed the delivery stall by
ordering the arm before the signal, and that ordering narrows this window
rather than closing it. Nothing is applied, because the obvious repair was
measured not to work and the sleep is a diagnostic rather than a fix. The
reproducer ships #[ignore]d so it is not lost, event_delivery.rs keeps its
released behaviour, and the work is queued as M26.12.

An earlier version of that test omitted the two consuming waits and passed
against the defect; it is recorded because it is the reason the finding looked
fixed when it was not.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings September 25, 2026 21:49

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Moderate findings remain in NUMA metadata/tests, replay accounting, benchmark coupling, resolution coverage, and trace-filter testing.

Review effort: Lite
Findings: None

Resolved since last review (5)

Both appeared only in the review overview's "Previously missed" section, in
code that had not changed, so neither had a thread to reply on. Both check out
against rules this repository states explicitly.

prealloc-cost.rs wrote through four println! call sites. The architectural
pre-step rule requires an output abstraction at the first occurrence, so the
table and the summary now go through one Report sink over an io::Write. The
rendered output is byte-identical in shape; only the destination became
separable.

SH-4.12 in CHECKLIST-ship-topology-and-queues.md was checked while its own
completion note recorded that ByL3 still owed a degradation test. That is the
checked-means-done rule's exact failure case: scheduled work marked done, with
the remainder surviving only as prose under a ticked box. SH-4.12 stays checked
for the conversion it did make, and the follow-up is spawned as SH-4.12.1,
naming the condition to assert -- "no outermost_partitioning_cache()" rather
than "no level 3 domain", since a test written against the old condition would
pass while checking the wrong rule.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings September 25, 2026 22:26
@MikeGrier

Copy link
Copy Markdown
Owner Author

Second review round, answering the two findings that arrived in the overview's "Previously missed"
section rather than as threads -- so there was nowhere to reply inline. Both landed in 95fc3e33.

Route measurement output through a single sink (prealloc-cost.rs:41) -- fixed. It wrote
through four println! call sites; the repository's architectural pre-step rule requires an output
abstraction at the first occurrence. The table and summary now go through one Report sink over an
io::Write. Rendered output is unchanged in shape; only the destination became separable. Verified by
compiling and running it.

Do not mark work complete while ByCache test is missing
(CHECKLIST-ship-topology-and-queues.md:695) -- fixed, and this one was the more useful of the
two. SH-4.12 was checked while its own completion note recorded that ByL3 still owed a degradation
test, which is precisely the failure the checked-means-done rule exists to prevent: scheduled work
marked done, with the remainder surviving only as prose under a ticked box.

Applied the rule's prescribed form rather than just unchecking: SH-4.12 stays checked for the
conversion it did make, and the follow-up is spawned as SH-4.12.1. The new item names the condition
to assert -- "no outermost_partitioning_cache()" rather than "no level: 3 domain" -- because a test
written against the old condition would pass while checking the wrong rule, which is the same
half-converted-predicate trap the original finding was about.


On the previous round's first finding, since it is now resolved and the resolution is not what the
thread suggests: the repair does not work. Signalling unconditionally leaves the reproducer failing
6 of 6. A 50 ms sleep between wait.arm and the signal makes it pass 3 of 3, as does
--features trace. So the wakeup is lost in a window after arming rather than never raised -- wider
than the report, and it bears on D-68, since M26.9 fixed the delivery stall by ordering arm before
signal and this says that ordering narrows the window rather than closing it.

Nothing is applied. event_delivery.rs keeps its released behaviour, the reproducer ships #[ignore]d
so it is not lost, and the work is queued as M26.12 with figures in UNRESOLVED-TEST-FAILURES.md.
Applying the suggested change would have looked like a fix while measurably changing nothing.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Unresolved correctness, cleanup, test-coverage, and documentation findings remain.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 Low severity

Open (1)
Previously missed (1)

In code that hasn't changed since last review

Medium severity Ensure temp files are cleaned up when verification panics

crates/​windows-ioring-sys/​examples/​epoch_log/​tests.rs:87

These removals run only after run_log(...).and_then(verify) returns; verify contains several assert!/assert_eq! calls, so any failed contract assertion panics before this cleanup and leaves all three temp files behind. Use a small drop guard (or catch_unwind with cleanup) so the self-test does not leak its process-scoped files on failure.

Comment on lines +701 to +704
- [ ] **SH-4.12.1** -- **Give `ByL3` a degradation test on the new rule.** Spawned from `SH-4.12`,
which converted the policy from `matches!(domain.kind, DomainKind::Cache { level: 3, .. })` to
asking `outermost_partitioning_cache()`, and whose own completion note recorded that the
degradation condition still owed a test.
@MikeGrier
MikeGrier merged commit 71331c8 into main Sep 25, 2026
37 checks passed
@MikeGrier
MikeGrier deleted the mikegrier/morechanges branch September 25, 2026 22:35
This was referenced Sep 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants