Skip to content

Report exactly the dead-letter drops the reset takes - #722

Merged
leynos merged 2 commits into
mainfrom
report-exact-dlq-drops
Sep 25, 2026
Merged

leynos merged 2 commits into
mainfrom
report-exact-dlq-drops

Conversation

@leynos

@leynos leynos commented Sep 25, 2026 •

Copy link
Copy Markdown
Owner

What

The throttled dead-letter drop diagnostic now takes the drop count with swap(0) and reports exactly what it took. It used to report the value its own fetch_add returned and then store(0), a separate load and store.

Why

RFC 0002 recorded Loom finding the reset inexact with three producers. The producer that reached the threshold reported its own count, then stored zero after increments it never saw, so drops were cleared without being reported and the "N dropped" log over-reported under contention. The user ruled to fix it with an atomic swap-and-report.

A logger that loses the race to another producer's swap takes zero and does not log, rather than printing dropped=0.

The model

every_drop_is_reported_or_still_counted in crates/wireframe-loom/tests/push_dlq.rs runs three producers against a logging threshold of two, for both priorities, and asserts that reported plus remaining equals the three drops on every interleaving. Only a Loom build keeps the running total of reported drops it reads (dlq_reported, through PushHandleProbe::dlq_reported_count); nothing is added outside cfg(loom).

Code make test-loom
main (report dropped, then store(0)) fails for both priorities: reported (2) plus remaining (3) must equal the three drops
this branch (swap(0)) all eight push models and both gauge models pass

The existing models are unchanged and still pass. RFC 0002 records the fix in place of the open question, and the developers' guide describes it.

Gates

make test, make lint, make test-loom, make check-fmt, make markdownlint (with make spelling) and make test-workflow-contracts pass locally. The hosted Advanced Tests run of make test-loom on this branch, run 36181691446 at e78d690, is green.

Summary by Sourcery

Ensure concurrent dead-letter drop diagnostics report each failed drop exactly once.

Bug Fixes:

  • Make throttled dead-letter drop diagnostics atomically take and report the exact number of drops, preventing losses and over-reporting under concurrent producers.

Enhancements:

  • Extend Loom coverage to verify that reported and outstanding dead-letter drops account for every failed delivery across interleavings.

Documentation:

  • Document the exact dead-letter reporting behavior and the Loom model validating it.

Tests:

  • Add high- and low-priority three-producer Loom models for concurrent dead-letter drop accounting.

Chores:

  • Record the dead-letter counter reset issue as resolved in RFC 0002 and update related developer guidance.

@coderabbitai

coderabbitai Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Team

Run ID: a3d94916-e110-496f-a610-54172e043ad2

📥 Commits

Reviewing files that changed from the base of the PR and between 9e850ad and e78d690.

📒 Files selected for processing (5)
  • crates/wireframe-loom/tests/push_dlq.rs
  • docs/developers-guide.md
  • docs/rfcs/0002-model-checking-the-write-loop.md
  • src/push/queues/handle.rs
  • src/push/queues/mod.rs
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • leynos/monotony (auto-detected)
  • leynos/rust-prover-tools (auto-detected)
  • leynos/mapsplice (auto-detected)
  • leynos/whitaker (auto-detected)
  • leynos/mdtablefix (auto-detected)
  • leynos/typos-config-builder (auto-detected)
  • leynos/nixie (auto-detected)
  • leynos/shared-actions (auto-detected)
  • leynos/mxd (auto-detected)
  • leynos/mxd-www (auto-detected)

Included review availability: 0 reviews are currently available. Your included PR review attempts over the past 7 days set your current allowance at 1 review per hour.


Summary

Use an atomic swap(0) to take the dead-letter drop count, and report only the count taken. Skip logging when another producer has already taken the count.

Add the Loom model every_drop_is_reported_or_still_counted. It checks that, for three producers and a threshold of two, reported drops plus drops remaining in the counter equals three for both priorities. Update the developers’ guide and RFC 0002 with the fix and invariant.

Pre-merge checks

Check Status
Local tests, lint, Loom, Markdown and contract gates Reported passing by the author
CI at the stated head Reported green by the author
Hosted Advanced Tests, run 36181691446 Reported passing all Loom models

Remaining work

No outstanding work was identified in the supplied information. Review finding counts are unavailable.

Walkthrough

The dead-letter diagnostic now atomically takes accumulated drops when its logging threshold is reached. Loom-only instrumentation tracks reported drops, and a Loom test checks that reported and remaining drops total three across interleavings.

Changes

Dead-letter drop reporting

Layer / File(s) Summary
Atomic drop reporting
src/push/queues/handle.rs, src/push/queues/mod.rs
The Loom-only counter and probe track reported drops. When the threshold is reached, the diagnostic atomically takes the accumulated count and logs that value.
Concurrent drop invariant
crates/wireframe-loom/tests/push_dlq.rs, docs/developers-guide.md, docs/rfcs/0002-model-checking-the-write-loop.md
The Loom test checks that reported and remaining drops total three across interleavings. The guide and RFC document the swap behaviour and invariant.

Priority: ⬇️ Low

Change: Bug fix · Severity of issue fixed: Low

Merge Risk: ⚪ Minimal · up to e78d6

No further changes are identified before merge. Complete the usual pre-merge checks.

🚥 Pre-merge checks | ✅ 12 | ❌ 3

❌ Failed checks (3 warnings)

Check name Status Explanation Resolution
User-Facing Documentation ⚠️ Warning The PR changes user-visible DLQ warning behaviour. log_dlq_drop now logs the atomically taken count from swap(0) and suppresses a log when a producer takes zero, replacing the previous report-and-… Update the Push queues and connection actors section in docs/users-guide.md to state that throttled DLQ diagnostics report the exact number of drops taken by each reset, and that concurrent producers whose reset takes zero do not emit a…
Developer Documentation ⚠️ Warning Fail: the PR updates docs/developers-guide.md and RFC 0002, but the documentation set remains inconsistent with the changed behaviour. The unchanged docs/asynchronous-outbound-messaging-design.md … Update docs/asynchronous-outbound-messaging-design.md to describe the exact swap(0) reporting semantics, the three-producer Loom model, and dlq_reported_count. Update RFC 0002 to remove or close the resolved approximate-counter questi…
Observability ⚠️ Warning The change improves the DLQ warning. log_dlq_drop now logs the value returned by dlq_drops.swap(0), and the warning remains at the failed DLQ-send boundary with bounded configuration fields. Howev… Add a production counter such as wireframe_push_dlq_drops_total through src/metrics.rs, with a no-op implementation when metrics are disabled. Increment it once for every failed DLQ send, independently of the throttled warning. Use no l…
✅ Passed checks (12 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarises the main change: reporting the exact dead-letter drop count taken by the reset. No roadmap item or issue reference is required by the provided context.
Description check ✅ Passed The description directly explains the swap(0) change, its reason, the Loom model, test coverage, and validation results. It is clearly related to the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 9 functions across 3 files. (2 skipped: 2 …
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Testing (Overall) ✅ Passed Pass the testing check. Add and run a substantive Loom model with three concurrent failed DLQ sends, threshold two, and both priorities. Assert that reported drops plus remaining drops equals all thre…
Module-Level Documentation ✅ Passed Pass. The pull request changes three Rust modules, and each has module-level documentation. crates/wireframe-loom/tests/push_dlq.rs explains the Loom models and their relationship to the queues. `sr…
Testing (Unit And Behavioural) ✅ Passed Pass the testing check. The new Loom model drives the public PushHandle::try_push boundary through a full primary queue and full dead-letter queue. It starts three producer threads, runs every Loom …
Testing (Property / Proof) ✅ Passed Pass this check. The change introduces a concurrency invariant over producer orderings, and the PR adds the bounded Loom model every_drop_is_reported_or_still_counted. It explores three producers, a…
Testing (Compile-Time / Ui) ✅ Passed Pass. Treat this as runtime concurrency behaviour, not new compile-time behaviour. The PR adds a focused Loom model for both priorities and asserts that reported drops plus remaining drops equals thre…
Unit Architecture ✅ Passed PASS — The change preserves the query/command boundary. PushHandleProbe::dlq_reported_count and dlq_drop_count only perform atomic reads and are available only under cfg(loom). The command path …
Domain Architecture ✅ Passed Pass the Domain Architecture check. The change remains within the existing push-queue infrastructure boundary. It adds an atomic counter and swap(0) to make the dead-letter diagnostic exact, and it …
Full details: User-Facing Documentation

Explanation

The PR changes user-visible DLQ warning behaviour. log_dlq_drop now logs the atomically taken count from swap(0) and suppresses a log when a producer takes zero, replacing the previous report-and-reset sequence. Users and operators can observe this in the DLQ dropped frames ... dropped={reported} log. The existing docs/users-guide.md section only states that a DLQ has tunable logging cadence and that try_push supports dead-letter forwarding; it does not explain what the reported drop count means under concurrent drops or that a losing logger emits no zero-count record. The PR changes docs/developers-guide.md and an RFC instead, so it does not document the changed user-facing behaviour in the required guide.

Resolution

Update the Push queues and connection actors section in docs/users-guide.md to state that throttled DLQ diagnostics report the exact number of drops taken by each reset, and that concurrent producers whose reset takes zero do not emit a dropped=0 diagnostic. Keep the description aligned with the dlq_log_every_n and dlq_log_interval settings.

Full details: Developer Documentation

Explanation

Fail: the PR updates docs/developers-guide.md and RFC 0002, but the documentation set remains inconsistent with the changed behaviour. The unchanged docs/asynchronous-outbound-messaging-design.md still describes two Loom threads and only a threshold reset; it does not document the three-producer invariant, atomic swap(0), or PushHandleProbe::dlq_reported_count. RFC 0002 also leaves the resolved question “Is an approximate drop counter acceptable...” open and still says to take the counter decision. The developers' guide omits the new dlq_reported field from its Loom field table and says the models assert on the drop counter “and nothing else”, although the new model reads the reported counter.

Resolution

Update docs/asynchronous-outbound-messaging-design.md to describe the exact swap(0) reporting semantics, the three-producer Loom model, and dlq_reported_count. Update RFC 0002 to remove or close the resolved approximate-counter question and revise the recommendation. Update the developers' guide field table and model description so they include dlq_reported and the reported-plus-remaining invariant.

Full details: Observability

Explanation

The change improves the DLQ warning. log_dlq_drop now logs the value returned by dlq_drops.swap(0), and the warning remains at the failed DLQ-send boundary with bounded configuration fields. However, the change alters externally observable drop-reliability reporting without adding a production metric. dlq_reported exists only under cfg(loom), while the repository's production metrics API has no DLQ-drop metric. The throttled log is therefore the only production signal for failed DLQ sends.

Resolution

Add a production counter such as wireframe_push_dlq_drops_total through src/metrics.rs, with a no-op implementation when metrics are disabled. Increment it once for every failed DLQ send, independently of the throttled warning. Use no labels, or only bounded labels such as a fixed failure category. Keep the Loom-only counter for the interleaving test. Avoid using the frame payload as operational context in the warning; use stable fields such as the failure category, priority, and configured logging thresholds instead.


Count each drop; take the sum,
Let the next drops still arrive.
Three threads wander through the Loom,
Two may trigger logs to thrive.
Keep the counted total whole,
And let each interleaving roll.

Comment @coderabbitai help to get the list of available commands.

@sourcery-ai

sourcery-ai Bot commented Sep 25, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

The dead-letter drop throttle now atomically swaps the accumulated counter to zero and logs exactly the value it claims, preventing lost or over-reported drops under producer contention. New Loom coverage validates the invariant across interleavings and priorities, while Loom-only accounting and project documentation record the fix and its rationale.

Sequence diagram for atomic dead-letter drop reporting

sequenceDiagram
    participant ProducerA
    participant ProducerB
    participant DLQCounter
    participant Logger

    ProducerA->>DLQCounter: fetch_add(1)
    ProducerB->>DLQCounter: fetch_add(1)
    ProducerA->>DLQCounter: swap(0)
    DLQCounter-->>ProducerA: reported count
    ProducerA->>Logger: warn(dropped=reported)
    ProducerB->>DLQCounter: swap(0)
    DLQCounter-->>ProducerB: 0
    Note over ProducerB: No log when another producer won the swap
Loading

State diagram for dead-letter drop counter ownership

stateDiagram-v2
    [*] --> Accumulating
    Accumulating --> Accumulating: fetch_add(1)
    Accumulating --> Reporting: threshold reached
    Reporting --> Accumulating: swap(0) returns count
    Reporting --> Accumulating: swap(0) returns 0
    Reporting --> Reported: warn(dropped=reported)
    Reported --> Accumulating
Loading

Flow diagram for Loom dead-letter drop invariant

flowchart LR
    Producers[Three concurrent producers] --> Drops[DLQ drop counter]
    Drops --> Swap["Atomic swap(0)"]
    Swap --> Reported[Reported drops]
    Swap --> Remaining[Remaining counter]
    Reported --> Invariant[reported + remaining = 3]
    Remaining --> Invariant
    Invariant --> Pass[All interleavings and priorities pass]
Loading

File-Level Changes

Change Details Files
Make throttled dead-letter diagnostics atomically claim and report the exact drops being reset.
  • Replace the separate observed-count report and zero store with swap(0).
  • Skip logging when a concurrent swap leaves no drops to report.
  • Update the log and Loom-only accounting to use the atomically claimed count.
src/push/queues/handle.rs
Add Loom coverage proving concurrent drops are neither lost nor double-counted.
  • Generalize concurrent drop setup to any number of producer frames.
  • Model three producers at threshold two for both priorities.
  • Assert reported drops plus the remaining counter equals all produced drops.
crates/wireframe-loom/tests/push_dlq.rs
Document the atomic reset behavior and record the RFC follow-up as resolved.
  • Describe the swap-and-report semantics and Loom invariant in the developers guide.
  • Mark the previously inexact reset finding fixed and reference the new model.
docs/developers-guide.md
docs/rfcs/0002-model-checking-the-write-loop.md
Add Loom-only state and initialization for tracking reported diagnostic drops.
  • Add a cfg-gated reported-drop counter to the handle internals and probe.
  • Initialize the counter only in Loom builds.
src/push/queues/handle.rs
src/push/queues/mod.rs

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

codescene-access[bot]

This comment was marked as outdated.

@leynos
leynos marked this pull request as ready for review September 25, 2026 20:11
@leynos

leynos commented Sep 25, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai Is this pull request ready to merge at head e78d690? Please answer with a pre-merge checks table for this head, and name anything that still needs work.

The throttled dead-letter drop diagnostic now takes the count with swap(0) and reports exactly what it took, instead of reporting its own fetch_add result and then storing zero. The new Loom model every_drop_is_reported_or_still_counted (three producers, threshold two, both priorities) asserts that reported plus remaining equals the three drops. It failed on the old code (reported 2 plus remaining 3) and passes with the fix. The reported total it reads exists only under cfg(loom). RFC 0002 records the fix in place of its open question. make test, make lint, make test-loom and the Markdown and contract gates pass locally, CI is green at this head, and the hosted Advanced Tests run 36181691446 ran all the Loom models green.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@sourcery-ai sourcery-ai Bot 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.

Sorry @leynos, you've used your own review budget of 250,000 diff characters for the last 7 days.

You can request another review in 2 days and 20 hours by commenting @sourcery-ai review. Upgrade to get a review now.

@coderabbitai

coderabbitai Bot commented Sep 25, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -u
echo '=== checkout ==='
git rev-parse HEAD
echo '=== PR state ==='
gh pr view 722 --repo leynos/wireframe --json headRefOid,baseRefName,isDraft,mergeable,mergeStateStatus,reviewDecision,statusCheckRollup,url,updatedAt
echo '=== check runs at requested SHA ==='
gh api repos/leynos/wireframe/commits/e78d690ad1e35112080480aca5e8341b2bb2bd17/check-runs --jq '[.check_runs[] | {name,status,conclusion,html_url,app:.app.slug}]'
echo '=== combined status at requested SHA ==='
gh api repos/leynos/wireframe/commits/e78d690ad1e35112080480aca5e8341b2bb2bd17/status --jq '{state,statuses:[.statuses[] | {context,state,target_url}]}'
echo '=== hosted Advanced Tests run ==='
gh api repos/leynos/wireframe/actions/runs/36181691446 --jq '{id,head_sha,status,conclusion,html_url,name,event}'
echo '=== files in PR ==='
gh pr diff 722 --repo leynos/wireframe --name-only
echo '=== model outline ==='
ast-grep outline crates/wireframe-loom/tests/push_dlq.rs --items all --match 'every_drop_is_reported_or_still_counted|PushHandleProbe'
echo '=== references ==='
rg -n -C 3 'every_drop_is_reported_or_still_counted|dlq_reported_count|dlq_reported|swap\(0\)|RFC 0002|test-loom' crates/wireframe-loom/tests/push_dlq.rs crates README.md Makefile 2>/dev/null | head -220

Length of output: 10692


🏁 Script executed:

#!/bin/bash
set -u
echo '=== change diff ==='
gh pr diff 722 --repo leynos/wireframe -- crates/wireframe-loom/tests/push_dlq.rs docs/developers-guide.md docs/rfcs/0002-model-checking-the-write-loop.md src/push/queues/handle.rs src/push/queues/mod.rs 2>&1 || git diff main...HEAD -- crates/wireframe-loom/tests/push_dlq.rs docs/developers-guide.md docs/rfcs/0002-model-checking-the-write-loop.md src/push/queues/handle.rs src/push/queues/mod.rs
echo '=== file outlines ==='
ast-grep outline src/push/queues/handle.rs --items all --match 'PushHandle|PushHandleProbe|dlq_reported_count|dlq_drop_count'
ast-grep outline src/push/queues/mod.rs --items all --match 'Queue'
echo '=== source slices ==='
rg -n -C 12 'dlq_reported|dlq_drop_count|swap\(0\)|dead.letter|dlq_drop' src/push/queues/handle.rs src/push/queues/mod.rs | head -300
echo '=== hosted jobs ==='
gh api 'repos/leynos/wireframe/actions/runs/36181691446/jobs?per_page=100' --jq '[.jobs[] | {name,conclusion,steps:[.steps[] | {name,conclusion}]}]'
echo '=== PR review metadata ==='
gh pr view 722 --repo leynos/wireframe --json reviews,latestReviews --jq '{reviews:[.reviews[] | {author:.author.login,state,submittedAt}],latestReviews:[.latestReviews[] | {author:.author.login,state}]}'
echo '=== branch protection API ==='
gh api repos/leynos/wireframe/branches/main/protection --jq '{required_status_checks,required_pull_request_reviews,enforce_admins}' 2>&1 | head -70

Length of output: 22958


🏁 Script executed:

#!/bin/bash
echo '=== targeted code ==='
sed -n '1,235p' crates/wireframe-loom/tests/push_dlq.rs
echo '=== RFC and guide references ==='
rg -n -C 5 'swap\(0\)|every_drop_is_reported_or_still_counted|dropped|dead.letter' docs/rfcs/0002-model-checking-the-write-loop.md docs/developers-guide.md | head -160

Length of output: 16442


🏁 Script executed:

#!/bin/bash
set -u
gh pr view 722 --repo leynos/wireframe --json headRefOid,mergeable,mergeStateStatus,reviewDecision,isDraft,statusCheckRollup --jq '{headRefOid,mergeable,mergeStateStatus,reviewDecision,isDraft,checks:[.statusCheckRollup[] | {name:(.name // .context),status:(.status // .state),conclusion:(.conclusion // .state)}]}'
gh api repos/leynos/wireframe/commits/e78d690ad1e35112080480aca5e8341b2bb2bd17/status --jq '{state,statuses:[.statuses[] | {context,state}]}'

Length of output: 1553


@leynos, do not merge this head yet. A fresh GitHub check still reports UNSTABLE because the CodeRabbit status is pending.

Pre-merge check Result at e78d690ad1e35112080480aca5e8341b2bb2bd17
Head and draft state Head matches; PR is not a draft.
Merge conflicts GitHub reports MERGEABLE.
Drop-count change swap(0) supplies the reported count; a zero count produces no log.
Loom regression model Covers three producers and both priorities. The reported-total counter is limited to cfg(loom).
CI, CodeQL and CodeScene Passed.
Hosted Advanced Tests run 36181691446 Passed, including Run Loom models.
Local gates Reported as passing; not independently run here.
CodeRabbit status Pending.
GitHub merge state UNSTABLE.

Still needs work: Wait for CodeRabbit to finish, then confirm that GitHub reports the PR ready to merge. I could not verify branch protection requirements because GitHub denied access to those settings.

You are interacting with an AI system.

@leynos
leynos marked this pull request as draft September 25, 2026 20:22
codescene-access[bot]

This comment was marked as outdated.

The throttled "DLQ dropped frames" diagnostic reported `dropped`, the
value the reporting producer's own increment returned, and then reset
the counter with `store(0)`. Increments other producers made in between
were cleared without being reported, and drops the report counted could
be left in the counter too, so under contention the log over-reported.
RFC 0002 recorded Loom finding this with three producers.

The reporter now takes the count with `swap(0)` and reports exactly what
it took, with no separate load and store. A logger that loses the race
to another producer's swap has nothing left and does not log.

`every_drop_is_reported_or_still_counted` runs three producers against
a logging threshold of two and asserts that reported plus remaining
equals the three drops on every interleaving. Only a Loom build keeps
the running total of reported drops it reads, through
`PushHandleProbe::dlq_reported_count`. On the old code the model fails
for both priorities (reported 2 plus remaining 3); with the swap all
eight push models pass.

RFC 0002 records the fix in place of the open question, and the
developers' guide describes it.
Review found the documentation set still described the old counter:

- the outbound messaging design described two Loom threads and a
  threshold reset only; it now describes the swap-and-report, the
  three-producer invariant and `dlq_reported_count`;
- RFC 0002 still listed the approximate counter as an open question and
  recommended taking that decision first; both are closed;
- the developers' guide's Loom field table lacked the Loom-only
  `dlq_reported` total, and said the models assert on the drop counter
  and nothing else;
- the users' guide did not say what the `dropped` count in the
  throttled warning means, or that no `dropped=0` line is emitted.
@leynos
leynos force-pushed the report-exact-dlq-drops branch from 726f5e3 to ed59770 Compare September 25, 2026 20:37
@leynos
leynos marked this pull request as ready for review September 25, 2026 20:54
@leynos

leynos commented Sep 25, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai Is this pull request ready to merge at head ed59770? Please answer with a pre-merge checks table for this head, and name anything that still needs work.

Since your review at e78d690, which had no error rows, the documentation warnings are taken:

  1. docs/asynchronous-outbound-messaging-design.md describes the swap-and-report, the three-producer invariant, and dlq_reported_count.
  2. RFC 0002 no longer lists the approximate counter as an open question, and its recommendation records the decision as taken.
  3. The developers' guide's Loom field table lists the Loom-only dlq_reported total, and the model description no longer says the models assert on the drop counter alone.
  4. The users' guide says what the throttled warning's dropped count means, and that no dropped=0 line is emitted.

The branch is then rebased onto main, which gained only a workflow checkout change. The Rust under test is identical to e78d690, where the hosted Advanced Tests run 36181691446 ran every Loom model green, the new one included. CI is green at this head.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@sourcery-ai sourcery-ai Bot 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.

Sorry @leynos, you've used your own review budget of 250,000 diff characters for the last 7 days.

You can request another review in 5 days and 14 hours by commenting @sourcery-ai review. Upgrade to get a review now.

@coderabbitai

coderabbitai Bot commented Sep 25, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -u
echo '=== checkout and PR metadata ==='
git rev-parse HEAD
gh pr view 722 --repo leynos/wireframe --json headRefOid,baseRefOid,baseRefName,isDraft,mergeable,mergeStateStatus,reviewDecision,statusCheckRollup,url --jq '{headRefOid,baseRefOid,baseRefName,isDraft,mergeable,mergeStateStatus,reviewDecision,checks:[.statusCheckRollup[]|{name:(.name // .context),status:(.status // .state),conclusion:(.conclusion // .state)}],url}'
echo '=== checks on head ==='
gh api repos/leynos/wireframe/commits/ed59770a8d5bb5dc5a8c6ff460dcb8a104f6cb90/check-runs --jq '[.check_runs[]|{name,status,conclusion,app:.app.slug,html_url}]'
echo '=== combined status ==='
gh api repos/leynos/wireframe/commits/ed59770a8d5bb5dc5a8c6ff460dcb8a104f6cb90/status --jq '{state,statuses:[.statuses[]|{context,state,target_url}]}'
echo '=== hosted run ==='
gh api repos/leynos/wireframe/actions/runs/36181691446 --jq '{id,head_sha,status,conclusion,html_url,name}'
echo '=== PR files ==='
gh pr diff 722 --repo leynos/wireframe --name-only
echo '=== commits ==='
gh pr view 722 --repo leynos/wireframe --json commits --jq '[.commits[]|{oid:.oid,messageHeadline:.messageHeadline}]'
echo '=== documentation paths ==='
fd -i 'asynchronous-outbound-messaging-design.md|wireframe-users-guide.md|developers-guide.md|0002-model-checking-the-write-loop.md'
echo '=== working tree status ==='
git status --short

Length of output: 5509


🏁 Script executed:

#!/bin/bash
set -u
echo '=== exact Rust changes since previous head ==='
git cat-file -e e78d690ad1e35112080480aca5e8341b2bb2bd17^{commit} 2>/dev/null && git diff --stat e78d690ad1e35112080480aca5e8341b2bb2bd17 ed59770a8d5bb5dc5a8c6ff460dcb8a104f6cb90 -- src/push/queues/handle.rs src/push/queues/mod.rs crates/wireframe-loom/tests/push_dlq.rs || echo 'previous head unavailable locally; compare via API separately'
echo '=== Rust blob SHA on each head ==='
for rev in e78d690ad1e35112080480aca5e8341b2bb2bd17 ed59770a8d5bb5dc5a8c6ff460dcb8a104f6cb90; do
  echo "$rev"
  for file in src/push/queues/handle.rs src/push/queues/mod.rs crates/wireframe-loom/tests/push_dlq.rs; do
    printf '%s ' "$file"
    gh api "repos/leynos/wireframe/contents/$file?ref=$rev" --jq '.sha' || true
  done
done
echo '=== documentation change summary ==='
git show --stat --format='%h %s' ed59770a8d5bb5dc5a8c6ff460dcb8a104f6cb90
echo '=== document declarations and descriptions ==='
rg -n -i -C 3 'every_drop_is_reported_or_still_counted|dlq_reported_count|dlq_reported|swap\(0\)|dropped=0|approximate|drop counter alone|three.producer|three concurrent|throttled|drop.*report' docs/asynchronous-outbound-messaging-design.md docs/developers-guide.md docs/rfcs/0002-model-checking-the-write-loop.md docs/users-guide.md | head -220
echo '=== hosted advanced run jobs ==='
gh api 'repos/leynos/wireframe/actions/runs/36181691446/jobs?per_page=100' --jq '[.jobs[]|{name,conclusion,loom_steps:[.steps[]|select(.name|test("loom";"i"))|{name,conclusion}]}]'

Length of output: 55701


@leynos, yes—PR #722 is ready to merge at ed59770a8d5bb5dc5a8c6ff460dcb8a104f6cb90 based on the verified checks. My earlier reply left the Rust comparison and documentation unverified. Those checks are now complete.

Pre-merge check Result
Head and merge state Head matches. The PR is not a draft. GitHub reports MERGEABLE and CLEAN.
Review GitHub reports APPROVED.
Checks at this head build-test, CodeQL, CodeRabbit, CodeScene, security review, and analysis checks passed. The combined status is success.
Loom regression Advanced Tests run 36181691446 passed, including Run Loom models, at e78d690. The three affected Rust files have identical Git blob IDs at both heads.
Documentation The design document, RFC 0002, developers’ guide, and users’ guide contain the stated updates.

Still needs work: No blocker appears in the verified results. The hosted Loom run was not run again at this head; its result applies to the identical Rust files checked above.

You are interacting with an AI system.

@leynos
leynos merged commit 44aed17 into main Sep 25, 2026
11 checks passed
@leynos
leynos deleted the report-exact-dlq-drops branch September 25, 2026 21:03
leynos pushed a commit that referenced this pull request Oct 15, 2026
Add the Progress entries for the second compile repair and for the audit
of every file both sides of the merge changed.

The repair entry records the `E0271` on `tests/prepared_app/mod.rs`, the
ancestry probe that attributed it to a latent defect in the commit that
introduced the shared factory rather than to the rebase, and the clean
`cargo check` that followed.

The audit entry records how each of the ten dual-changed files resolves
to a deliberate union: the two prepared-application test files that both
sides restructured, the scheduler carrying the target's layout with this
branch's counters, and the clean unions in the manifests and module
facades. It also records the content-level check that `#722`'s
dead-letter fix survived the move into `dlq.rs`, which SHA ancestry
cannot answer once a rebase has replayed the commits, and the two stale
path references that belong to the base lag rather than to this branch.

Plan only; no behaviour changes.
leynos pushed a commit that referenced this pull request Oct 16, 2026
Add the Progress entries for the second compile repair and for the audit
of every file both sides of the merge changed.

The repair entry records the `E0271` on `tests/prepared_app/mod.rs`, the
ancestry probe that attributed it to a latent defect in the commit that
introduced the shared factory rather than to the rebase, and the clean
`cargo check` that followed.

The audit entry records how each of the ten dual-changed files resolves
to a deliberate union: the two prepared-application test files that both
sides restructured, the scheduler carrying the target's layout with this
branch's counters, and the clean unions in the manifests and module
facades. It also records the content-level check that `#722`'s
dead-letter fix survived the move into `dlq.rs`, which SHA ancestry
cannot answer once a rebase has replayed the commits, and the two stale
path references that belong to the base lag rather than to this branch.

Plan only; no behaviour changes.
leynos added a commit that referenced this pull request Oct 18, 2026
* Add observability scaffolding for runtime ownership baselines (#639)

Establish the measurement primitives the four Epic #635 hot paths need
before their ownership topology changes, so baselines can be captured
against the current implementation and compared afterwards.

Add three pieces of instrumentation:

- `pool_scheduler_task_spawns()` exposes a process-wide count of
  scheduler worker tasks spawned by `PoolScheduler::kick`. It lives in a
  new `scheduler_spawns.rs` rather than `scheduler.rs` because adding it
  inline pushed that module to 403 lines, past the workspace's 400-line
  limit, and past whitaker's `module_max_lines` lint. The separate
  module mirrors the existing `connection::counter` arrangement.

- `wireframe::test_helpers::connection_startup` holds the counting startup
  fixtures — `ConnectionStartupInstrumentation`,
  `StartupFixturePlan`, `TransformCountingMiddleware`, and
  `counted_app_factory` — that were previously private to
  `tests/prepared_app.rs`. It lives in the `wireframe` crate rather than
  `wireframe_testing` because `Transform` is declared with
  `#[async_trait]` and `wireframe_testing` does not depend on that crate.
  All four prepared-app test crates now share one definition.

- `wireframe_testing::benchmark_alloc_support` extracts the counting
  global allocator and scoped `count_allocations` window from
  `benches/codec_performance_alloc.rs`, leaving each benchmark binary to
  declare only its one-line `#[global_allocator]` static.

Signal is a plain atomic counter rather than a `metrics` gauge because
benchmark binaries install no recorder, which would make a `metrics`
counter unobservable exactly where it is needed.

The counts are exact rather than inferred from timing, so they can be
asserted directly once the benchmark harness lands.

* Add runtime ownership benchmark support modules (#639)

Add measurement helpers for the four hot paths named by issue #639,
mirroring the existing `codec_benchmarks` arrangement so no second
benchmark framework is introduced:

- connection actor drain, with cancellation disabled, triggered before
  the run, and triggered mid-drain from a `before_send` hook;
- push handle sends, split into uncontended and overflow regimes;
- application connection startup, for the per-connection factory and
  prepared-root topologies;
- client pool acquisition, uncontended and contended under the FIFO and
  round-robin fairness policies.

Each module exposes its scenarios as data and a single fallible
`measure_*` entry point, so a benchmark binary, a test, or an ad-hoc
experiment can drive the same code, and scenarios stay comparable across
the ownership change.

They sit behind a new `runtime-benchmarks` feature, which enables
`wireframe/test-support` and `wireframe/pool`; gating keeps
`cargo check -p wireframe_testing` working with default features.

A failing test exposed that `PushHandle::try_push` returns `Ok(())` for a
frame a drop policy routes to the dead-letter queue, so a single
"delivered" counter would conflate "reported success" with "queued".
`PushMeasurement` therefore reports `accepted`, `drained`, and
`dead_lettered` separately, which also makes each regime's signature
legible.

Three further defects in the new doc examples were found by running the
crate's doctests directly: a mis-aliased constant, an assertion naming
the wrong scenario, and an allocation assertion that no doctest binary
can satisfy because the counting allocator must be installed as the
process global allocator. The surviving assertion is that the counting
window closes, which holds regardless.

* Clear clippy and spelling findings in benchmark support (#639)

Scrutineer's first gate run on 9bb6b90 was red at two of the five gates,
and both vetoes were real.

`make lint` stopped at clippy with six errors. The `as isize` casts in
`PoolMeasurement::alive_task_delta` trip `cast_possible_wrap`, so the
conversions now use `cast_signed`, which documents the intent that a task
count is small and non-negative. The two `%` expressions used to cycle a
frame or route identifier trip `integer_division_remainder_used`, which
this workspace denies outright; both become power-of-two masks, which
preserve the only property the callers rely on, that consecutive values
differ, without a division. The three-tuple returned by `build_queues`
trips `type_complexity` and is now the named `PushFixture` alias. Finally,
`Err(PushError::QueueFull) | Err(_)` trips `unnested_or_patterns`; the
catch-all already covered the named variant, so the catch-all is the
honest form.

Clippy aborts the lint target before whitaker runs, so whitaker's verdict
from that run was UNKNOWN rather than passing. This commit is what makes
a green lint claim checkable.

`make markdownlint` failed at its spelling prerequisite on `mis-aliased`
and `summarised`. The former becomes "incorrectly aliased"; the latter
becomes "summarized" because `typos.toml` is generated from a shared
en-GB-oxendict base that pins the -ize form, so hand-editing it is not an
option.

The plan document also gains the findings that produced these fixes, so
the next reader does not rediscover the denied-lint surface by failing a
gate.

* Correct pool acquisition counters and clear spelling gate (#639)

Measure the acquisition phase rather than the whole pool lifecycle, and
replace two documented claims that direct measurement falsified.

`PooledClientLease::drop` calls `notify_capacity_available`
unconditionally, and `try_acquire_immediately` arms even a fast-path
lease with a release handle, so every release reaches
`PoolScheduler::kick`. A temporary probe measured spawn deltas of 1, 1,
and 2 for bursts of 1, 16, and 256 acquisitions across pool sizes 1, 4,
and 16 — never 0. The module documentation asserted the opposite and is
corrected.

The live-task gauge was also measuring the wrong phase: `bb8` connects
eagerly, so bracketing the whole measurement reported a delta equal to
the pool size. `pool_once` now builds and closes the pool outside the
bracketed region and samples the gauge only around acquisition.

Add cases pinning the true contract: a burst spawns at least one worker,
the spawn count is bounded by the number of acquire/release transitions,
contended acquisition reaches the slow path, and a closed pool leaves no
construction tasks behind. The last was verified by mutation — sampling
before construction fails all three cases with deltas of 2, 5, and 17 —
so the bracket is load-bearing rather than cosmetic.

Also fix `characterised` to `characterized` in the ExecPlan, which
failed the spelling prerequisite and so prevented `markdownlint-cli2`
from running at all. The gate now completes end to end over 119 files.

* Add runtime-ownership baselines and fix spurious disconnect (#639)

Measure the four scopes issue #639 names before #637 and #638 change
their ownership topology: connection-actor event processing, the push
handle send path, application connection startup, and client pool
acquisition. The recorded values are the baseline the later refactors
are compared against, so the harness is built to outlive them.

Two Criterion binaries back the four groups.
`runtime_ownership_baselines` records timing;
`runtime_ownership_baselines_alloc` records allocation counts and is
separate because `#[global_allocator]` is per-process, so installing the
counting allocator would tax every timing sample in the sibling. Both
drive the same workload helpers, so a scope cannot be added to one and
silently skipped by the other.

The workload helpers live in `wireframe_testing` beside the existing
codec benchmark support, behind a new `runtime-benchmarks` feature,
rather than introducing a second benchmark framework. Each scope exposes
a workload enum, a measurement struct, and a `measure_*` function
returning `Result`, mirroring `codec_benchmarks`.

Acceptance criteria this discharges:

- Factory invocation and middleware transformation counts are asserted
  directly, not inferred from timing: one shared prepared root builds
  once and transforms once per route per layer, while the
  per-connection factory path rebuilds and re-applies for every
  connection.
- Pool scheduler task creation is observable through a process-wide
  counter in `kick`, so the spawns a pool scenario caused are read
  rather than guessed.
- The allocation harness proves it can resolve what criterion 5 asks of
  it: a zero-allocation closure reports exactly zero, a 1024-byte
  allocation reports non-zero, and per-item `Arc` construction reports
  one allocation per item more than cloning a shared `Arc`. The last is
  the measurement that decides whether nested-`Arc` removal would move
  the count at all.

Building the contended pool scope surfaced a pre-existing library defect,
fixed here because it is small, local, verified, and user-visible:
`PoolScheduler::acquire_for_handle` returned a spurious
`ClientError::disconnected()` whenever it dequeued a waiter whose session
had already stopped listening, telling a caller the pool had gone away
while it was still serving traffic. Two windows reach it — a contender
that observes the last busy slot and returns while a worker concurrently
services that same waiter, and `close()` racing in-flight acquires. The
fix keeps the healthy discarded lease instead of converting it into an
error. Only one line of `src/client/pool/scheduler.rs` differs from
`main` on this branch and it is the new counter increment, so the defect
predates this work.

The pool counter is documented as a bracket rather than a zero, because
measurement showed the obvious claim is false: `PooledClientLease::drop`
notifies the scheduler unconditionally, so a release reaches `kick` even
when nothing is waiting and an uncontended burst spawns between one and
`2n` workers.

The push workload label no longer appends the priority lane, which every
scenario name already states; the overflow scenarios all send high, so
it spelled `high` twice.

No CI job runs `cargo bench`: the results are ratios and reviewed
evidence, not brittle absolute timing thresholds. `make
bench-runtime-ownership` is the reproduction command. The baseline table
and regression policy land in `docs/runtime-ownership-baselines.md`.

* Record the pool acquire-path defects instead of patching them (#639)

Measuring contended pool acquisition exposed two pre-existing defects in
the client pool's acquire path: a spurious `disconnected` error, and an
intermittent stall in which every contender waits with the queue still
populated. Both reproduce against unmodified library code.

A one-line candidate repair in `PoolScheduler::acquire_for_handle` was
implemented and measured, then rejected. It removed the spurious error
but converted it into an intermittent deadlock: `make
bench-runtime-ownership` hung during Criterion warm-up, with the main
thread parked in `iter_custom` and every tokio worker parked. Trading a
fast failure for a hang is strictly worse, so the change is reverted and
`src/client/pool/scheduler.rs` returns to its original arm.

The stall is inherent to sustained contention rather than to the
harness. With the repair reverted it still reproduces, at first-stall
bursts ranging from 272 to 18069 across both fairness policies, rebuilt
and long-lived pools, fresh and reused handles, and 8 or 16 contenders.
A control separates it from the fixture's task leak: 20000 uncontended
iterations leaked the same four tasks per iteration and never stalled.

Because a stall occurs during Criterion warm-up, contended acquisition
cannot be timed at all, so it is now reported as a bounded `tracing`
observation rather than a benchmark group. `CONTENDED_BURST_TIMEOUT`
turns a stall into a reported error so a benchmark or `cargo test
--all-targets` run cannot block forever. Uncontended acquisition stays
Criterion-timed.

The regression test asserting the spurious disconnect was fixed becomes
a documented known-defect tolerance, since it passes only because its
workload is too light to reach the stall, and asserting correctness
there would be false.

Fixing the acquire path belongs to ADR 013 and issue #647; this change
records the baseline, which is #639's stated purpose.

* Apply rustfmt to the runtime-ownership benchmark support

The previous commit added the known-defect tolerance and the spawn-counter
assertions without running `cargo fmt`, so `make check-fmt` reported three
diffs: two in the new contention test (an over-wrapped function signature and
a string literal that fits on one line) and one in a `PushWorkload::label`
doctest.

Pure formatting; no behaviour changes.

* Report contended pool allocations as an observation

The allocation benchmark's first real run failed outright:

    allocation timing pass failed: 1 of 16 contenders failed; first:
    transport error: connection closed by peer

That is the pre-existing spurious-disconnect defect in the pool acquire path,
not a fault in the harness. The counting window itself had already succeeded —
it records one window, not a Criterion sample series — but the timing pass that
follows it in the same group runs far more iterations, so it reliably reaches
the interleaving that fails.

The contended scope is therefore removed from `allocation_scopes` and reported
once after `Criterion::final_summary` through a bounded observation, exactly as
the timing binary already reports it. The allocation count is unaffected: it
still comes from a counted window, so it is the same measurement with only the
repetition and the timing dropped.

The observation installs a subscriber filtered to this binary's own target, so
the connection-startup workloads' per-connection INFO spans do not bury the
count.

See `docs/runtime-ownership-baselines.md` and issue #647.

* Record the runtime-ownership baselines (#639)

Capture the four scopes the issue names and transcribe the numbers into
`docs/runtime-ownership-baselines.md`, replacing the two placeholders.

The baseline was taken with `make bench-runtime-ownership` on the named
machine: 24 timing labels, 8 allocation labels, 2 arc-sensitivity labels,
and 2 contended observations, with no panic and no error.

Notable readings, all of which the document now records:

- The startup pair is the sharpest contrast in every table. Over four
  connections and two routes, the per-connection factory path costs
  21.881 us and 408 allocations against the prepared root's 10.578 us and
  160 allocations, because it evaluates the factory four times and runs
  sixteen middleware transformations instead of one and four. At a single
  connection the two paths cost about the same, which is the point:
  preparation is a fixed cost the prepared root amortizes.
- The nested-`Arc` sensitivity pair recorded 1 allocation for 256 shared
  clones against 256 for 256 fresh constructions, a delta of exactly
  `ARC_PROBE_ITEMS - 1`. That is the direct answer to acceptance
  criterion 5 and the proof that the counter resolves a per-item
  allocation.
- An uncontended pool burst spawns 1 scheduler task for 1 or 16
  acquisitions and 2 for 256, identically at pool sizes 1, 4, and 16. The
  hidden cost is a per-idle-to-busy-transition wake-up, not a
  per-acquisition one.

Also record the contended observations and why they are observations
rather than timings: the allocation binary died on its first capture with
`1 of 16 contenders failed; first: transport error: connection closed by
peer`, reached from unmodified benchmark code driving unmodified library
code. Criterion's timing pass runs orders of magnitude more iterations
than an allocation window, so it hits the acquire-path interleaving
nearly every run. The count was never in doubt; it comes from a counted
window that completed before the failure.

Mark the ExecPlan's capture item complete.

* Clear the runtime-ownership gate failures (#639)

Fix every fault the commit gates reported for the baseline harness, and
retire a flaky assertion the gates could not have caught reliably.

Clippy rejected eleven sites. The benchmark registration closures were
nested too deeply to read, so the route/connection grid is now built as
data by `startup_workloads` and each group's timed body moved into its
own `time_*` function. The two reporting `match` arms exceeded the
cognitive-complexity budget because a `tracing` macro expands to an
`enabled` conditional; each arm is now a named function. The allocation
binary's workload table is an `AllocationWorkloads` struct with
documented fields, which also removes its extra parameter count. The
probes that cannot fail now return `()` rather than wrapping `()` in
`Ok`, removing two `unnecessary_wraps`, and the probe pointer type is a
named `ArcProbe` alias rather than an inline `type_complexity` site.

The `test-doc` gate failed on `src/test_helpers/connection_startup.rs`:
its example used `?` across `WireframeError` and `PrepareError`, which do
not unify. The example now returns `Box<dyn Error>`, the one type both
convert into.

`block_on` in the helper tests returned a `Result` through `expect`,
which `expect_used` denies outside a `#[test]`; it now propagates the
runtime-construction error as the workloads' own `String` so all
fourteen call sites keep the `Result` they already match on.

The live-task assertion was load-sensitive and would have failed spuriously
under the full suite. The gauge counts *every* task on the runtime, so on a
multi-thread runtime it measures the whole scheduler: sampling an idle→busy
burst there produced deltas from −12 to +10 with a long tail above +2 in a
third of runs, at every pool size, on a workload whose own churn is a fixed
constant. On a current-thread runtime the same workload recorded only −1
and 0 over 180 samples. That one case therefore runs on a current-thread
runtime, where the reading answers the question asked, and the bound is a
non-scaling ceiling rather than a fit to the observed values: the property
is that the delta does not grow with `pool_size`, which is what the
construction-bracket mutation demonstrates.

Settlement is now established rather than assumed. A worker woken by the
final release exits on its first poll, so a gauge read taken immediately
after can count a task already on its way out; `settled_alive_tasks` waits
for the gauge to hold steady across consecutive polls, and reports a
non-settling gauge rather than yielding once and hoping.

The gauge's validity domain and the reasoning behind the bound are recorded
in `docs/runtime-ownership-baselines.md` under instrumentation sensitivity,
so a future reader does not re-derive them from a flake.

* Split the runtime-ownership benchmark support modules (#639)

Whitaker's `module_max_lines` lint enforces the 400-line module limit as a
hard error, and all four benchmark-support modules written for this issue
broke it: `client_pool_benchmark_support` at 822 lines,
`push_benchmark_support` at 605, `connection_actor_benchmark_support` at 463,
and `connection_startup_benchmark_support` at 418. Because `make lint` runs
rustdoc, then clippy, then whitaker, the finding only became visible once the
two earlier steps were green.

Split each module into `mod.rs` (documentation, declarations, re-exports),
a data module (`workload.rs` or `scenario.rs`), a `measurement` module that
drives the data, and `tests.rs`. Where a module contained two genuinely
different control flows, those get their own submodules rather than sharing
one body: `push/measurement/{uncontended,overflow}.rs` and
`client_pool/measurement/{phase,contended}.rs`.

A shared body would have obscured which branch a regression sits in, so the
split is an improvement in its own right, not only a way to satisfy the lint.
It also moves each regime's rationale next to that regime instead of leaving
one long module header to explain both. No `#[allow(module_max_lines)]` was
used; `AGENTS.md` reserves lint suppression as a last resort.

The split is internal: every consumer — the two bench binaries and
`tests/runtime_ownership_benchmark_helpers.rs` — imports only from the module
root, so no public path changed and the recorded baselines still compare.
All 21 files are now under 240 lines, and the four `module_max_lines`
violations are confirmed cleared by a full `make lint` run.

Two hazards surfaced while splitting, now recorded in the ExecPlan. A name
used only in a Rustdoc intra-doc link trips `unused_imports` once the code
that used it lands in a sibling file, so such links carry the full path.
And one doc link in `client_pool_benchmark_support/mod.rs` reaches into a
private submodule; it is rewritten as a plain-text path, since rustdoc would
reject the link were that feature ever enabled for the docs build.

Also records both findings in the ExecPlan's Surprises section and ticks the
corresponding Progress item.

* Qualify Rustdoc links that the split left dangling (#639)

Splitting the benchmark-support modules into submodules moved several items
out of the scope of the documentation that linked to them, and rustdoc rejects
the resulting links under `-D warnings`:

- a public module's docs linking to a *private* submodule
  (`rustdoc::private-intra-doc-links`); and
- `[`PushError::QueueFull`]` and `[`Envelope`]` links to items that are no
  longer imported in the file that names them
  (`rustdoc::broken-intra-doc-links`).

The gate missed all sixteen because `make lint` documents the workspace
*without* `runtime-benchmarks`, and the benchmark-support modules are
`#[cfg]`-gated behind that feature. Measuring the unimplemented regime
directly (`RUSTDOCFLAGS="--cfg docsrs -D warnings" cargo doc -p
wireframe_testing --features runtime-benchmarks --no-deps`) reported sixteen
errors; the same command now reports none.

Fix each site to keep the readable name while resolving: paths into private
submodules are plain code spans rather than links, and links to `wireframe`
items carry the full path. `scenario.rs`'s `PushMeasurement` field had the
same defect as the table above it, which an earlier pass over the same hazard
had missed, so this was not a one-site mistake.

The previous commit's message described this repair as done; it was not, and
that commit is left alone rather than rewritten. This one records it honestly.

* Record the lint gate's blind spot for gated modules (#639)

The sixteen Rustdoc errors fixed in 9985ed1 were invisible to `make lint`
because its rustdoc step runs `cargo doc --workspace --no-deps` while the
clippy step beside it runs `--all-features`. Modules behind a `#[cfg]` feature
are therefore compiled by clippy but never documented, so their intra-doc
links go unchecked.

Record the measurement, the sixteen-error transcript it produced, and the
one-word remedy — adding `--all-features` to the rustdoc command, which exits
0 on this tree — so the next agent working in a feature-gated module knows the
standard gate will not catch dangling links.

The remedy is recommended as its own base pull request rather than applied
here: it changes a gate every branch and agent runs, and this branch's diff
should stay about benchmark harnesses.

* Resolve the CodeRabbit findings on the benchmark baselines (#639)

Three review findings, all verified against the source before acting.

The public rustdoc for `pool_scheduler_task_spawns` promised callers
could assert that an uncontended burst "does not create additional
scheduler tasks", contradicting the module doc twenty lines above it,
which explains the counter is a bound rather than a zero because every
lease release reaches `kick`. The rustdoc now states the real bound.

The contended pool workload rendered `acquisitions_256` in its label
even though the contended path never reads `PoolWorkload::acquisitions`;
a burst is driven by a per-contender round count instead, so the label
advertised a parameter the run did not honour. Promote that count from a
private `CONTENDED_ROUNDS` constant to public `CONTENDED_ROUNDS_PER_CONTENDER`
workload data, state it in the contended label as `rounds_8`, and
document on the field that it is read only in the uncontended regime. A
parameterized test pins both regimes' labels so they cannot drift back
together. Only the identifier changed: the recorded baseline counters
stand, and a re-run confirmed the burst still grants 128 leases (16 x 8),
which is what the corrected prose in `docs/runtime-ownership-baselines.md`
now says.

Two integration tests called `.expect` on a single contended burst,
which the acquire path can fail spuriously at roughly one burst in 913
at 16 contenders. Both that test and its sibling in the support crate
now retry a bounded eight attempts before failing, putting a false
failure below one in 10^27 while still reporting a pool that fails every
attempt.

The 400-round known-defect test could not flake -- it records failures
rather than propagating them -- but its loop had no wall-clock bound, so
a pathological run could hold the gate for an unbounded time. Bound it
with a time budget and a floor on attempts rather than reducing the
round count, which is deliberately matched to the documented failure
rate. Its doc comment also claimed it would start failing once the
defect is fixed, which is false; the assertion tolerates zero failures.

* Close the second CodeRabbit pass on the baselines (#639)

Resolve all five findings from the follow-up review. Four are
documentation contradictions that the lint gates cannot see, because
each is prose or a comment disagreeing with the code beside it rather
than a type or formatting error.

Close the pool on the acquisition error path
(`client_pool_benchmark_support/measurement/phase.rs`). A contended
burst that times out has already been detached: the timeout drops
`contended_round` and with it that function's `JoinHandle`s, which
detaches the contender tasks instead of aborting them. Those tasks hold
`PoolHandle` clones, so dropping the pool left its inner alive and
nothing shut it down, and the stalled contenders kept racing against
the next iteration's budget and the callers' bounded retries. The
acquisition result is now carried past `pool.close().await`, so
shutdown wakes the queued waiters with `disconnected` and ends them.
This cannot block: `shutdown` is synchronous, and the detached handles
keep the inner alive, so the following drop is an `Arc` decrement
rather than a teardown. The settled-gauge read moved into
`settled_observation` so it still runs only on the success path, where
quiescence is meaningful.

Correct the remaining four sites:

- `tests.rs` claimed the task-gauge delta is clean because the pool is
  closed before `pool_once` returns. Closure happens *after* the
  `alive_after` sample and cannot affect it; what makes the delta clean
  is that `alive_before` is already taken past construction, so the
  eagerly connected tasks cancel on both sides of the subtraction.
- `docs/runtime-ownership-baselines.md` said a default actor workload
  leaves both queues under back-pressure. Each lane receives exactly
  its capacity, so neither reaches it.
- `tests/runtime_ownership_benchmark_helpers.rs` pointed at the wrong
  home for the V-O6 counter assertions. They live in
  `verify_allocation_harness` in the alloc bench, not in
  `wireframe_testing`'s unit tests, which can only observe that the
  window opens and closes.
- The ExecPlan's decision log still recorded leaving the support
  modules over 400 lines. A later discovery in the same document
  falsified that, so the entry is marked superseded and the Progress
  snapshot no longer quotes a headroom that subsequent commits changed.

The `module_max_lines` episode is worth stating plainly, because the
misreading was mine: the "whitaker exited 0 with no diagnostic" evidence
came from a run where an earlier step in the same target had already
failed, so the whitaker step never ran. A gate that aborts early
produces no evidence about the step after it.

* Record the close-out of the baseline work (#639)

Tick the ExecPlan's last two items and summarize what the second review
pass found. The plan is the hand-over document for this branch, so it
should say what a reader would otherwise have to reconstruct from the
commit log: that both review passes are clear, that all five gates are
green, and that one of the five findings was a real defect rather than a
wording fix.

The defect is worth naming here because it is the only finding on this
branch that changes behaviour: a failed acquisition left the pool open,
so a timed-out contended burst's detached contenders outlived their
iteration.

* Record the rebase and the module split in the plan (#639)

The ExecPlan's living sections did not yet describe the rebase onto
`origin/main`, the three conflicted steps, or the module extraction that
followed it.

Add Progress entries for the replay, the `make lint` failure the rebase
exposed, and the green gate run at the resulting head. Add two Decision
log entries: one for resolving the `module_max_lines` failure by
extracting the in-module tests to a sibling `scheduler_tests.rs` using
the `#[path = "..."]` pattern `main` introduced in #710, and one for
resolving both `scheduler.rs` conflicts to the target's structure on the
grounds that this branch's series contains an add-then-revert pair whose
net effect is a single added line.

Both entries record why the chosen resolution beat the alternatives, so
a later reader does not have to reconstruct the reasoning from the diff.

* Note that the rebase target changed the measured pool path (#639)

The rebase target's #535 reworked client-pool waiter state transitions
and, per its own commit message, deliberately stopped clearing the
servicing guard from the immediate-lease resolver. That is the mechanism
this branch identified as the cause of the contended-acquisition stall,
and #535 landed after this branch was cut, so every recorded reading
predates it.

Add the discovery to the ExecPlan so the distinction is not lost: the
timings and allocation counts remain true statements about the code state
they name, but the "known defects" narrative is a claim about live
behaviour and cannot be left standing unverified when the code under it
changed. #535 does not obviously close the spurious `disconnected`
window, since both the old and new forms still map a failed send to
`Err(disconnected)`, so the two modes need re-testing separately.

A bounded experiment re-measures the stall on the rebased tree; the
recorded defect material is reconciled to its verdict.

* Record the range-diff audit of the rebased series (#639)

The replay was accepted without auditing why four of sixteen commits
differed from their originals. Add the two decisions that disposition
each difference.

The spawn counter now lands in commit 1 rather than commit 5. The net
change to `scheduler.rs` is still exactly one added line, so nothing was
lost; the earlier placement is the better grouping, since commit 1 is
where the counter's scaffold is introduced.

The other three differences are target-driven renames and context drift:
`#710` renamed `src/test_helpers.rs` and `tests/prepared_app.rs` to
`mod.rs` forms, and the add-then-revert pair means commits 5 and 6 start
from a different `scheduler.rs` text on each side and cannot replay
identically. Each was inspected individually rather than accepted because
`range-diff` reported it.

* Spell out which pool defect the recorded panic names (#639)

The baselines document states that the allocation binary's first capture
aborted with `transport error: connection closed by peer`, leaving the
reader to work out whether that names the spurious `disconnected` error
or the stall. The two are easy to conflate and the mapping is verifiable,
so state it.

`ClientError::disconnected()` builds an `io::Error` reading "connection
closed by peer", and the `WireframeError::Io` arm renders it as
"transport error: {error}". The recorded panic is therefore the spurious
disconnect. A stall has a different signature entirely: no contender
error, only a burst that never completes.

This matters because the rebase target changed the code path that
produces the stall while leaving the disconnect path alone, so the two
modes now have to be reasoned about separately.

* Record the H1 verdict on the pool acquire-path stall (#639)

An experiment re-tested the recorded acquire-path stall against the
rebase target's #535, which reworked the waiter state transitions. The
prediction that the stall no longer reproduces was **not falsified**:
24,000 rounds per fairness policy at the recorded 8-contender shape
produced zero stalls, against roughly 60 expected per policy if the
recorded one-in-400 rate still held.

The spurious `disconnected` defect reproduced throughout, at 327 of
24,000 FIFO rounds and 38 of 24,000 round-robin rounds. The FIFO rate is
materially above the 1-in-400 previously recorded for that shape; the
experiment cannot separate a genuine change from machine and load
profile, so it is recorded as an open question rather than asserted.

Verify the mechanism structurally rather than from the commit message:
`stop_servicing`, the only writer that clears `is_servicing`, now has one
caller, `take_next_waiter_or_stop`, and the immediate-lease arm reaches
`next_waiter`, which never clears the flag.

State the scope of the result: a one-sided test cannot prove absence, and
the experiment covered the 8-contender shape only, so the 16-contender
shape is re-tested before any documentation claim is narrowed.

* Separate the two pool acquire-path defects in the recorded baselines

The rebase moved this branch onto `main`'s #535, which reworked the
client-pool waiter state transitions on exactly the path whose two
defects the baselines recorded as a single narrative. That narrative no
longer holds, so re-test it rather than leave a claim about live
behaviour standing on code that has since changed.

Re-tested at the recorded shape (pool size 4, 16 acquisitions, 8 rounds
per contender, 8 and 16 contenders), zero stalls occurred in 80,000
rounds — while the spurious `disconnected` reproduced throughout, at
roughly 1 round in 73 on FIFO at 8 contenders. The two defects are
therefore now recorded separately:

- The spurious `disconnected` is live and remains the dominant
  observable failure at this scope.
- The stall no longer reproduces. It is kept as an observation, marked
  pre-#535, because it is the evidence that motivated the rework and the
  only measurement of its original frequency.

Frame the stall as "no longer reproducible" rather than "fixed". A
zero-count is one-sided: 0 in 16,000 bounds the per-round rate only at
roughly 1 in 5,300, so absence is not established. `CONTENDED_BURST_TIMEOUT`
is kept for the same reason it was written: it converts a hang into a
bounded error, and the disconnect defect still fails bursts, so removing
it would trade a bounded failure for an unbounded one.

The re-tested disconnect rates are recorded as observations with their
round counts, not as a revised rate. The 8-contender FIFO figure sits
materially above the pre-#535 measurement while the 16-contender figures
do not, and a genuine shift is not distinguishable from machine and load
profile in data taken on a loaded machine.

Correct three sites that claimed issue #647 "remains open on the
strength of" the disconnect defect. #647 is a rework proposal that does
not cite either defect as its motivation, so the corrected text states
only what is verifiable: both defects live on the path it proposes to
rework, and neither is its stated motivation.

Gates: `make check-fmt`, `make lint` (rustdoc, clippy, whitaker),
`make typecheck`, `make test` (1306 passed), `make markdownlint`,
`make nixie` — all green on a content-stable tree.

* Record the publication and the gate closure in the plan

Add the three Progress entries the close-out needs: the defect
reconciliation, the force push, and the gate-coverage gap the
documentation edits opened.

That last one is the entry worth keeping. The first `make check-fmt` and
the rustdoc step of `make lint` both ran before the final Markdown
rewrites and the module doc edit landed, and since a broken intra-doc
link is a rustdoc-only failure mode, the earlier green did not cover the
content that was actually committed. Re-running both gates on the frozen
commit with the tree hash verified identical before and after closed it.
A green gate is only evidence for the tree it read.

* Sync the plan's series count with the published branch

The publication entry named 23 commits, which was accurate when the push
happened and stale by the time the entry above it was written: the
reconciliation and close-out commits took the series to 24. Correct the
count, the head it names, and add the re-run range-diff audit so the
plan's account of the series matches what is on the remote.

* Correct the pool measurement boundaries and recapture baselines (#639)

Address the seven items raised in the PR #756 review: four measurement
defects and three completeness gaps.

Measurement defects:

- Build and warm the pool outside the timing and allocation windows, and
  report construction cost separately. `PoolHarness` now owns that bracket
  and is passed to the measuring code, so nothing inside a window can
  construct anything. This removes a `pool_size`-scaling TCP handshake
  term from every timed iteration.
- Scope the churn counters to the pool rather than the process. A global
  `AtomicU64` gave a correct reading only when one pool was live, and the
  benchmark binaries run several. `PoolTaskChurnObserver` now lives in the
  pool's own shared state (`src/client/pool/churn.rs`), replacing
  `scheduler_spawns.rs`. Serializing the assertion sites would not have
  sufficed: the counters are read inside the measurement window.
- Sample contended bursts as a distribution rather than one elapsed value,
  with failures counted and reported beside the successes. A failed burst
  is never retried into a timing sample.
- Separate awaited back-pressure from the uncontended regime with an
  observable counter, and add an explicit no-backpressure assertion.

Completeness gaps:

- Refresh the current pool baseline and identify its source revision,
  preserving the original capture under `d28b54e`.
- Justify the startup workload sizes and add a larger case.

Also split `measurement/distribution.rs` into a directory module: whitaker's
`module_max_lines` lint rejected the file at 450 lines against the 400
limit. The split is at the type/tests seam and changes no public surface.

The recapture is confounded by machine state — the load averaged 10-15 on
24 threads from other work — so scopes whose code is byte-identical moved
1.3-2.1x between captures. No cross-capture timing delta is attributed to
the correction; the bracket's existence is instead proved by a
deterministic zero-iteration equality in the allocation binary.

Two claims in the documentation were found to describe experiments that
were never performed, and are corrected to cite the evidence actually
gathered: a validation "across 20 readings" of the bracket check, and a
pool allocation spread that mixed two measurement boundaries.

Refs #639

* Serialize the allocation-window tests to stop a process-global race

`benchmark_alloc_support::tests` has two tests that both assert on
`ALLOCATION_COUNTING_ENABLED`, a process-global flag. Under the default
multi-threaded harness one test's open window could be observed by the
other's closing assertion, failing about one run in five.

The race reproduced in isolation, which ruled out interference from the
rest of the suite and identified the pair as racing each other.

Mark both `#[serial]` and add `serial_test` as a dev-dependency of
`wireframe_testing`. `#[serial]` rather than a re-scope, because the flag
genuinely is a property of the process: the counting allocator is
installed once as `#[global_allocator]` and is not owned by any pool.

No new external crate enters the tree. `serial_test = "4.0.1"` is already a
workspace dev-dependency and already resolved in `Cargo.lock`; the only
lockfile change is one edge in `wireframe_testing`'s dependency list.

Refs #639

* Remove the duplicated group and runtime setup the baseline files share

CodeScene's advisory code-health gate failed on code duplication in both
files this branch adds, and both were real:

- `benches/runtime_ownership_baselines.rs` repeated the same
  `sample_size`/`measurement_time` pair in all four `benchmark_*` groups.
  Extracted to `baseline_group`, so one place decides the pinned sample
  budget rather than four that can drift apart silently.

- `tests/runtime_ownership_benchmark_helpers.rs` had two `block_on`
  flavours whose build-and-drive tails were identical apart from which
  runtime they constructed. Extracted to `run_on`, which takes the builder.

Both are behaviour-preserving: the 36 helper tests pass, and all 39
Criterion benchmarks register and run under `--test`, with the contended
sampling observations unchanged.

Note that the gate was failing before this round of work as well, so this is
not a regression introduced by the measurement corrections; it is the
branch's own new code being brought up to the advisory rule.

Refs #639

* Register the Criterion groups from one path

Declare each of the four runtime-ownership benchmark scopes as a
`BenchScope` value — group name, workloads, throughput selector, and
measurement helper — and register all four through a single `run_scope`.
The four `benchmark_*` functions were near-identical: each opened a
group, looped its workloads, registered a `bench_function`, and called
`finish()`. They are now `actor_scope`, `push_scope`, `startup_scope`,
and `pool_scope`, returning values rather than driving Criterion.

CodeScene's advisory duplication gate flagged the module at 9.38. The
finding was real, not a false positive: every other bench file in the
repository scores 10.00, including `benches/codec_performance.rs`, whose
`benchmark_encode` and `benchmark_decode` use the same registration
idiom but number only two, below the three-function threshold. The
module now scores 10.00.

Two rejected alternatives are worth recording. Extracting only the
shared four-token syntax tail changes no logic — that tail is the
Criterion API's own punctuation — and left the score unchanged. A first
attempt at a `register_workloads` helper scored *worse* (9.09) and drew
a new "Excess Number of Function Arguments" finding from the six-argument
registration, so it was reverted rather than kept as churn.

`BenchLabel` exists because the four workload types are distinct and the
shared driver needs one name to reach each type's existing inherent
`label` through. The implementations only forward, so the labelling
stays owned by the type that knows its own fields.

The refactor is behaviour-preserving, which matters because the recorded
baseline is keyed by benchmark id. Group names (`actor/drain`,
`push/send`, `startup/connection`, `pool/acquisition`), registration
order, workload iteration order (the actor scope still nests
`ACTOR_CANCELLATIONS` outside `ACTOR_FRAME_COUNTS`), throughput
selectors, and measurement helpers are all unchanged, so the recorded
figures still key the same scenarios.

* Record the duplication fix and the stale-base attribution

Add a Decision log entry for registering the four Criterion groups from
one path, including the two rejected alternatives and the evidence that
the CodeScene finding was real rather than a false positive.

Add a Surprises entry for two reusable findings. The local `cs` CLI
reproduces CodeScene's delta analysis, which took the bench module from
9.38 to 10.00 in one local iteration instead of three push round-trips.
The companion caveat is that `cs delta <ref>` compares against the ref as
it is now: on a branch whose base has advanced it re-reports changes the
branch never made. `cs delta origin/main` named
`src/client/tracing_config.rs` as new duplication, but that file is not
in the pull request, is byte-identical from the merge base to `HEAD`, and
was last touched by an unrelated 2025 commit — the local `origin/main`
had simply moved two commits ahead. That is the same stale-base failure
mode the restacked-ancestor attribution rule already covers.

* Add the plan's outcomes section and restate the scope breach

The ExecPlan's preamble declares `Outcomes & retrospective` among its
mandatory living sections, but the section itself was never written. Add
it, covering the seven acceptance criteria, the four measurement defects
and three completeness gaps the review raised, the lessons worth carrying
into the #637 and #638 refactors, and the two residual items left
unrepaired on purpose.

Restate the scope-breach figure from its canonical source. The previous
entry counted a union of committed, working-tree, and untracked paths,
which double-counts a path present in more than one set and is not
reproducible from a clean tree; it read 57 paths and roughly 9,050 net
lines. The merge-base numstat gives 56 paths and 11,247 net lines (11,610
added, 363 deleted) against `ef858aa`. The verdict is unchanged — the
ceiling was already breached by a wide margin — but the figure now comes
from a command a reader can re-run.

* Apply mdtablefix's wrapping to the outcomes section

`make check-fmt` reflowed three paragraphs in the new outcomes section
(+13 -13). `mdtablefix` is the wrap authority for this repository, so its
choice of breaks governs; a hand-wrap that keeps every line within 80
columns still fails the gate when the breaks differ.

Pure rewrap: the prose is byte-identical once newlines are normalised,
and the file stays at 2484 lines.

* Record the outcomes section and the wrap fix in the plan

Note in `Progress` that the mandatory `Outcomes & retrospective` section
was added, that the scope-breach figure was corrected to its canonical
merge-base numstat, and that `check-fmt` then caught a hand-wrap in the
new prose which `make fmt` reflowed.

Record why no code gate was invalidated: every path changed since the
code-gated head is a `.md`.

* Correct a spelling violation CI caught in the progress entry

`make spelling` in CI rejected `normalised` and demanded `normalized`.
The estate dictionary is en-GB-oxendict but pins `normalize` as the
correct form, so the `-ise` spelling is a violation regardless of the
general rule.

The local gate run that preceded this commit was green because it ran
before the offending line existed; the line was written afterwards and
not re-gated. Re-ran `make spelling` over the corrected tree (exit 0) and
confirmed the gate still detects this class of error with a seeded
negative control, which reported exit 2 and the identical diagnostic.

* Record the spelling-gate lesson in the plan

The gate mandates `-ize` where the convention's name suggests `-ise`, and
a gate run only vouches for the tree it ran against. Also record that
the gate reads tracked files only, so an untracked probe file cannot
serve as a negative control.

* Rebuild the lockfile after the rebase onto the harden-lint stack

The rebase onto `harden-lint-connection-fragment` conflicted in
`Cargo.lock`: the target pins `thiserror` at 2.0.20 where this branch's
commits were resolved against 2.0.21. The base branch's lockfile was taken
verbatim, as instructed, and regenerated here.

`wireframe_testing` gained a `serial_test` dev-dependency in the
allocation-window work, so the rebuild adds that one edge to its package
stanza. Nothing else changes: the crate was already resolved and already a
workspace dev-dependency, and the target's `thiserror` pin survives the
rebuild.

Refs #639

* Record the restack onto the connection-fragment layer (#756)

Add the ExecPlan entries for the restack onto
`origin/harden-lint-connection-fragment` and the validation that followed,
keeping the plan usable as the continuing record of this work.

Progress records the boundary evidence (`ef858aa..84bfbf3` as the owned
series, proven rather than guessed), the per-commit `range-diff` result, the
two conflict resolutions whose obvious answer was wrong, and the post-restack
defect the merge introduced.

Surprises records that the ambient `weave` merge driver corrupts files while
exiting cleanly, with the four observed modes and the artefact scan that
follows from them.

Decision log records the per-conflict authority rule, and the reasoning for
treating the local `mdtablefix` failure as tool drift rather than repairing
it here.

Two environmental findings are recorded with evidence instead of being folded
into the branch: `make check-fmt`'s `mdtablefix` half is red only because this
host runs 0.6.1 against CI's pinned 0.6.0, and the spelling gate rewrites
`typos.toml` from a newer shared dictionary on a pristine tree. Neither is
this branch's to fix, and committing either would publish an unrelated change.

* Record the merge-hybrid audit in the plan (#639)

Add the Progress entries for the second compile repair and for the audit
of every file both sides of the merge changed.

The repair entry records the `E0271` on `tests/prepared_app/mod.rs`, the
ancestry probe that attributed it to a latent defect in the commit that
introduced the shared factory rather than to the rebase, and the clean
`cargo check` that followed.

The audit entry records how each of the ten dual-changed files resolves
to a deliberate union: the two prepared-application test files that both
sides restructured, the scheduler carrying the target's layout with this
branch's counters, and the clean unions in the manifests and module
facades. It also records the content-level check that `#722`'s
dead-letter fix survived the move into `dlq.rs`, which SHA ancestry
cannot answer once a rebase has replayed the commits, and the two stale
path references that belong to the base lag rather than to this branch.

Plan only; no behaviour changes.

* Record the churn-counter guard evidence in the plan (#639)

Note that the 23 client-pool benchmark-support tests pass, and why the two
capacity-wait assertions are worth running together: one holds
`capacity_waits == 0` when a single caller never contends, the other
requires `capacity_waits >= contenders` when a burst holds the pool
empty. Recording the counter in the `else` branch satisfies both, while
the position the merge produced satisfies only the second.

Plan only; no behaviour changes.

* Record the test and formatter gate evidence in the plan (#639)

Add the `make test` result for the rebased candidate: exit 0 across 83
test binaries, 1313 passed, 0 failed, with both compile repairs confirmed
by the absence of `E0271` and `E0382` and by all three `prepared_app`
binaries running.

Record why `make check-fmt` reports `docs/execplans/adopt-shared-
mutation-workflow.md` and why the file was deliberately left alone. The
flag is tool drift, not content: the local `mdtablefix` is 0.6.1 while CI
pins 0.6.0. Local 0.6.1 rejects the target's inherited copy and accepts
the copy on `origin/main` that `b479338` re-wrapped, and CI run
`35963755221` passed on the very commit whose tree carries the identical
blob. Reformatting would edit a file this branch never touched, in a tree
that will re-inherit it from main when the stack rebases.

* Record the typecheck, validator and CI-log evidence in the plan (#639)

Add the `make typecheck` result for the final head: exit 0, no errors,
with only the expected sccache-unreachable notices. Record the
standalone `mbake validate Makefile` pass that the branch's Makefile edit
requires.

Strengthen the support for leaving the `make check-fmt` flag alone by
quoting the CI job log rather than inferring from the run conclusion. The
log shows CI installing `MDTABLEFIX_VERSION: 0.6.0` and then reporting
`116 files left unchanged` with nothing flagged. Reconcile the counts to
show the two tools disagree on exactly one input, a stale copy of a file
neither this branch nor CI changed:

    CI (0.6.0):      116 unchanged + 0 flagged
    local (0.6.1):   117 unchanged + 1 flagged  = 118
    118 - 116 = 2 = the 2 Markdown files this branch adds

* Record the lint result and terminal gate verdict in the plan (#639)

Add the `make lint` result for the final head: exit 0 across all three
stages — doc (9m03s), clippy (13m06s) and Whitaker (17m32s) — with zero
warnings under `-D warnings`.

Add a terminal gate table so the acceptance state is readable without
parsing the per-gate Progress entries, and state explicitly which
revision each gate saw. The code gates did not all run at one revision,
which is sound only because `git diff --name-only 701f343 HEAD` returns
zero non-`.md` paths; recording the revisions is what makes that checkable
rather than asserted. `check-fmt` is listed as drift rather than waived.

* Record the rebase onto main in the plan (#639)

The branch was retargeted from the connection-fragment layer onto
`origin/main` at the user's request. Record the replay in the living
plan: boundary evidence, the eight conflicts and their resolutions, the
three commits dropped as empty, and the two `harden-lint` findings the
replay falsified.

The boundary moves to `OLD_BASE` `69d8fdc7`, `OLD_HEAD` `661fce8a` and
`TARGET` `3c50de2a`, with an owned series of 44 commits and 0 merges.

Mark the two superseded `harden-lint` claims in place rather than
deleting them. The claim that the previous target deliberately dropped
`from_app_serves_concurrent_tcp_connections_from_one_prepared_root` is
false: the test is absent at `OLD_BASE` but present at both `TARGET` and
the new head, in `tests/prepared_app_tcp.rs` line 91. The companion
claim that the `39221cf` bound mismatch was a latent defect in
`5f31996`, not a rebase artefact, is likewise superseded: it was an
artefact of the previous target's module layout and does not arise
against main.

Add two discoveries and two decision-log entries. A restack's recorded
resolutions are evidence about its target, so replaying onto a different
target invalidates them and they must be re-derived rather than
replayed. A lockfile conflict can be narrower than the lockfile diff:
taking main's side wholesale satisfied the letter of the lockfile rule
while shipping a lockfile that cannot satisfy `--locked`, because the
one edge this branch contributes fell outside the reported hunk.

Extend the retrospective with the replay's cost and outcome, and correct
the `test-doc` residual to say the base rather than the merge base, now
that `src/server/runtime/server.rs` is byte-identical between the target
and the branch head.

* Fix the spelling violation that failed the hosted gate (#639)

`build-test` run 38947673516 failed at the en-GB-oxendict spelling step
on the American spelling of "artefact" at this plan, line 2872. The line
was written in `4366ec1`, after the last local spelling run, so the fix
had never been gated.

The new progress entry also records the decisive evidence for the
pending lint question: on the same run the `Lint` step passed before the
spelling step failed, so rustdoc, Clippy and `whitaker --all` are green
on the published head in an environment that does not contend with this
host's compile admission. Two local attempts had been aborted by
admission refusals rather than by any diagnostic pointing at repository
source.

That verdict also settles the 400-line question. The four benchmark and
test files over 400 lines are not flagged by whitaker, so the stale
review finding citing the guide's 400-line figure describes guidance
rather than an enforced rule for those paths. The observation is
recorded with that distinction so a later reader does not mistake an
unenforced guideline for a passing gate.

`make spelling`, `make check-fmt` and `make markdownlint` all exit 0 on
the corrected tree; `typos.toml` is unchanged by the regeneration.

* Repair the review findings on the baselines PR (#639)

Address the 21 inline review comments on PR #756. Fifteen were implemented;
six were rebutted with evidence, and the rebuttals are recorded in the
ExecPlan's Decision log.

- The push capacity counter records a wait on the first `Pending` poll of
  the send future rather than at the `Full` refusal. A refusal only says a
  poll *would* park, so counting it reported suspensions that never
  happened. The new `await_send_capacity` helper holds the poll loop, and
  `record_first_pending` holds the predicate, keeping both under the
  repository's nesting threshold without a lint suppression.
- `PoolHarness` no longer folds warm-up churn into its totals: the delta a
  caller reads now covers only the phases that were bracketed.
- `measure_push` folds every round through `SendOutcome::accumulate`, so
  all of its counters describe one run instead of the last round.
- The allocation bench registers stable Criterion IDs and publishes its
  counts as `tracing` events. A count inside the ID made every run a new
  benchmark with nothing to compare against and a fresh directory under
  `target/criterion`. The pool replay returns its measured `Duration` so
  construction and teardown sit outside the timed span, and the
  zero-iteration guard runs on a dedicated current-thread runtime.
- The timing bench emits one push wait-counter event per workload from
  `main`, after `final_summary`, rather than from `time_push` where
  Criterion calls it on every iteration.
- Docs and tests were corrected in step: the broken `churn.rs` link, the
  `scheduler_spawns` `0..=2n` bracket with its idle-scheduler precondition,
  the `drained` and `awaited_capacity_waits` field docs, the allocation-ID
  prose, the misleadingly named zero-spawn test, and a floor that now rests
  on successful bursts with a non-vacuity assertion.

The four 400-line findings and two related CodeRabbit findings are rebutted
as guidance-only: whitaker's `module_max_lines` cannot see out-of-line
`mod` bodies or crate roots, and the hosted lint passed on the exact
published head with those file sizes.

* Split the over-limit benchmark and test files (#639)

Four files added by this branch exceeded the repository's 400-line limit
for code files. Split each along its natural seam, keeping every new file
well under the limit (the largest is 265 lines):

- `benches/runtime_ownership_baselines.rs` becomes a thin `main.rs` plus
  `scenarios`, `driver`, and `observe`.
- `benches/runtime_ownership_baselines_alloc.rs` becomes a thin `main.rs`
  plus `scopes`, `pool_bracket`, `probes`, and `reporting`.
- `tests/runtime_ownership_benchmark_helpers.rs` becomes a `mod.rs` root
  plus `harness`, `startup`, `actor`, `push`, and `pool`.
- `client_pool_benchmark_support/tests.rs` becomes a `tests/mod.rs` root
  plus `labels`, `uncontended`, `contended`, and `sampling`.

The split preserves behaviour. All 112 single-line string literals, 136
whitespace-normalised literals, 39 `assert!`, 36 `assert_eq!`, 12
`#[test]`, and 27 `#[case::` invocations are unchanged, as are the
Criterion group names, sample size, and measurement time. The counting
allocator is still installed at the allocation binary's crate root,
because `#[global_allocator]` is per-process.

`Cargo.toml` gains explicit `path` entries for the two `[[bench]]` targets
and a new `[[test]]` entry for the helpers crate, since Cargo does not
auto-discover `tests/<name>/mod.rs`.

Also fix the zero-iteration pool bracket guard, which review flagged as a
correctness bug: it used to run after `Runtime::new()` had started worker
threads, so a worker could allocate inside a window read as pool
construction. It is now split in two. The zero-iteration half builds and
drops its own current-thread runtime before any other runtime exists, and
asserts the bracketed count is zero while the unbracketed count is
positive; the measured half asserts the measured phase is non-zero
through the benchmark runtime.

* Fix the spelling gate failure in the plan (#639)

The hosted `build-test` job rejected `normalised` in the progress entry
added after the local Markdown gate had already run, so the local run
could not have caught it: the wording was written afterwards.

Write `normalized`, which is what the shared en-GB-oxendict policy
requires, and re-run `make spelling`, `make fmt`, `make check-fmt`, and
`make markdownlint` to confirm the file is clean. `typos.toml` is
unchanged.

---------

Co-authored-by: leynos <leynos@rohga>
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.

1 participant