Skip to content

feat: add denial-classification seam to keeper - #36

Open
mfw78 wants to merge 6 commits into
feat/quote-staleness-ledgerfrom
feat/denial-classification-seam
Open

feat: add denial-classification seam to keeper#36
mfw78 wants to merge 6 commits into
feat/quote-staleness-ledgerfrom
feat/denial-classification-seam

Conversation

@mfw78

@mfw78 mfw78 commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

What

  • Adds Venue::classify_denied(detail: &str) -> RetryAction in crates/videre-sdk/src/client.rs, a provided method that defaults to RetryAction::Drop for every denial, letting a venue marker override how its denied faults fold into the generic keeper retry path.
  • Threads a DenialClassifier (fn(&str) -> RetryAction) through Keeper in crates/videre-sdk/src/keeper.rs: Keeper::new defaults to the coarse DROP_DENIED mapping, Keeper::for_venue::<V> binds both the venue id and V::classify_denied so a keeper cannot route to one venue while folding another's denials, and Keeper::with_classifier allows an explicit override.
  • Folds every reconcile refusal arm (terminal fault, rate-limited-with-hint, transient) through one private refusal_disposition helper against the new Disposition enum (Release / Park { until } / left pending), replacing the previous three separately-derived branches, so reconcile and the retry-action mapping can no longer disagree.
  • Extends crates/videre-macros/src/venue_marker.rs (and a small lib.rs touch) to keep the emitted venue marker impl body open, so a marker's macro-generated impl can carry an overridden classify_denied alongside the macro's own generated items; adds a test pinning that a marker override reaches the emitted impl.
  • Documents the drop-class rule directly on classify_denied: only a drop-class action (Drop or DropOnRepeat) retires a stranded reservation, TryNextBlock leaves the reservation RESERVED and reconcile re-POSTs the refused body every tick.

Why

The generic keeper reconcile path previously treated every non-accepted, non-signing-required refusal with one fixed terminal/transient split, so a venue had no way to tell the keeper that a particular denial reason should back off, drop, or be retried differently from the coarse default. classify_denied gives venues that hook without making Keeper or reconcile generic over the venue (the classifier is carried as a plain fn pointer), and collapsing the refusal handling into one refusal_disposition closes the gap where the rate-limit arm, the terminal check, and the retry-action mapping could previously drift out of sync.

Closes #11

Testing

All commands run in the repo's nix develop shell (rustc 1.94.0, cargo-nextest 0.9.127) on a clean checkout of feat/denial-classification-seam (3096cae), based on origin/feat/quote-staleness-ledger. Touched crates: videre-sdk, videre-macros (5 files, +485/-68; no Cargo.toml/Cargo.lock delta).

  • cargo clippy -p videre-sdk -p videre-macros --all-targets --all-features -- -D warnings - clean, no warnings.
  • cargo test --doc -p videre-sdk -p videre-macros --all-features - videre_macros 0 passed, videre_sdk 0 passed (no doctests declared); ok.
  • RUSTDOCFLAGS='-D warnings' cargo doc --no-deps -p videre-sdk -p videre-macros - clean, docs generated.
  • just build-modules (required because videre-macros changed) - echo-venue, echo-client, echo-keeper, flaky-venue all built for wasm32-wasip2 --release.
  • cargo nextest run -p videre-sdk -p videre-macros - full pass.

AI Assistance

Implementation by claude-fable-5, red-team review by claude-opus-5, PR description by claude-sonnet-5.

@mfw78

mfw78 commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

Deferred red-team finding filed: #40 (RunReport buckets by RetryAction, so a DropOnRepeat retirement tick counts as retried, never dropped). Shepherd adoption of the seam is tracked in nullislabs/shepherd#669.

mfw78 added 4 commits August 6, 2026 15:26
Hang denial classification off the Venue marker as a provided static
classify_denied, defaulting to the coarse every-denial-drops mapping.
The keeper carries it as a plain fn pointer (DenialClassifier), so
Keeper and reconcile stay non-generic over the venue: Keeper::for_venue
takes both the id and the classification from the marker, reconcile
takes the classifier as a parameter, and the free retry_action::<V>
delegates its denied arm to the marker.

A softened denial still releases its journal reservation. The grace is
a watch-level ledger the Retrier keeps, and the reconcile pass has no
repeat counter, so holding the reservation would re-POST the same
refused body every tick forever. Releasing hands the watch back to the
next poll, where the Retrier grants one block and then retires it. So a
future EIP-1271 signer on the generic sweep inherits the one-block
grace instead of dropping on the first refusal, and it still
terminates.

No venue symbol enters videre-sdk; the seam is name-free. Shepherd
adoption is filed as nullislabs/shepherd#669.

Implements #11.

AI Assistance: Claude Code used for implementation and tests.
The client form of #[videre_sdk::venue] rejected any item in the impl
body, so a marker declared through the macro could not override the new
Venue::classify_denied. Pass the body through and reject only a
hand-written ID or Body, which the attribute still supplies.

Split the shape checks out of expand so they are testable without a
module.toml next to the manifest.

AI Assistance: Claude Code used for implementation and tests.
…ition

The reconcile pass matched the rate-limit hint itself and recomputed the
park window that retry_action already derives, and the guard test wrote
the mapping a third time. Route all three through refusal_disposition,
which folds a fault under the classifier into the nexum-sdk Disposition.

This also gives a classified denial its backoff. A venue that classifies
a denial to RetryAction::Backoff previously fell through to the
leave-it-RESERVED arm, so the pass ignored the window and re-POSTed the
refused body on every tick.

AI Assistance: Claude Code used for implementation and tests.
RUSTDOCFLAGS=-D warnings fails on a public item linking to the private
refusal_disposition. Name it in plain code font instead.

AI Assistance: Claude Code used for the fix.
@mfw78
mfw78 force-pushed the feat/denial-classification-seam branch from 0256e81 to 3d53f31 Compare August 6, 2026 15:30
mfw78 added 2 commits August 6, 2026 15:45
Deleting the override splat from the marker expansion kept the suite
green: the shape gate is tested, the emission is not, so a venue's
classify_denied could silently revert to the coarse default and
resurrect the bug the seam exists to fix.

Split the emission out of expand so it is testable without a module.toml
next to the manifest, and assert the override survives it. Rename the
test fixtures off the cow venue name while here.

AI Assistance: Claude Code used for the test and the split.
A venue implementer sees RetryAction and can reach for TryNextBlock on a
denial it expects to clear. Only a drop-class or backoff action retires
or paces a stranded reservation, so that choice pins the marker RESERVED
and re-POSTs the refused body on every tick. Say so at the seam.

AI Assistance: Claude Code used for the doc.
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