Skip to content

feat: ruleset_list(sort=) for the server's active-first order (4.5.0) - #324

Merged
vhmartinezm merged 9 commits into
developfrom
DN-8445-ruleset-list-active-first
Sep 15, 2026
Merged

vhmartinezm merged 9 commits into
developfrom
DN-8445-ruleset-list-active-first

Conversation

@vhmartinezm

@vhmartinezm vhmartinezm commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

What

ruleset_list(sort=None) — an opt-in pass-through for the API's new active-first ruleset
order, where rulesets carrying a live hunt link come back first. Omit it and the call is
byte-for-byte what it was.

Written on the async method, which is canonical, and regenerated into the sync mirror with
scripts/regenerate_sync.py.

Why the 4.5.0 bump rides this PR

The standing exception: the sibling raises its polyswarm_api>= floor to the version that
introduces the surface, and its CI resolves this repo from source by branch name. The paired
PR is polyswarm-cli#270, which pins polyswarm_api>=4.5.0 for ruleset_list(sort=); its
pipeline installs $POLYSWARM_API_ARCHIVE/$CI_COMMIT_BRANCH.zip and that branch is pushed
here under the identical name, so the floor is satisfiable before 4.5.0 reaches PyPI.

Two things callers have to know

The rank is the stored hunt link, which is wider than what livescan_id renders from.
The server orders on the link and serializes the id under a stricter predicate, so a legacy
row whose hunt was stopped without clearing the link leads the list while rendering a null
id. Read the field to decide what is running, never the position in the list.

The key is mutable, unlike the id-desc default. A ruleset whose live hunt stops
part-way through a walk falls back into the idle block below the cursor and is yielded
twice; one started part-way through moves above the cursor and is skipped for the rest of
that walk. Starting fresh from the first page does not avoid it — it is a property of the
walk, not of a stale cursor.

The generator streams pages and deliberately does not dedupe. It is the shared streaming
helper behind every list endpoint, and giving it an unbounded id set for the sake of one
caller-opted sort is the wrong trade. Callers consuming more than one page dedupe by id.
All of this is documented on the method, mirrored into the sync client, and recorded in the
endpoints table.

exclude_favorites

A second opt-in filter, appended to the signature so a positional caller keeps working. It is the
inverse of favorites_only and the server refuses the pair. It exists for clients that render the
favorites as their own list: leaving them in the paginated list too makes a page repeat a row or
come back short.

Verification

Full suite green (228), plus recorded live-request tests for the sorted walk on both the sync
and async clients. Both ordering reads — sorted and default — are polled in a
membership-tolerant form, so a lagging read replica retries rather than raising out of the
poll or asserting on a None.

…r-side

`ruleset_list(sort='active_first')` asks the server for rulesets with a running
live hunt first — as recorded by the server's live-hunt link, the same one
`livescan_id` renders from — newest first within each block
(`GET /v3/hunt/rule/list?sort=active_first`). Unset sends no `sort`, so the
request stays byte-compatible with the pre-sort contract and the list keeps its
newest-first default. The SDK never re-orders rows: the list is
keyset-paginated, so a client-side sort would reorder one page and misrepresent
the rest; a page's `offset` is only valid under the same `sort`, and the server
refuses a cursor minted under the other order.

Canonical change in aio/api.py; api.py is the regenerated unasync mirror
(scripts/regenerate_sync.py, ruff on PATH). YaraRuleset.list already forwards
arbitrary keywords through core._params, so the resource needs no change.

Tests on two tiers. Pure-unit pins the wire shape on both transports, the
omitted default, composition with the filters, and that `_next_page` carries
`sort` onto page 2. Live-e2e (sync + async, cassettes recorded against a stack
running the server branch) pins what no builder test can: the server actually
applies the order — this test's running ruleset precedes its newer idle one
under `sort='active_first'` and follows it under the default — and refuses an
unknown sort rather than ignoring it.
The CLI client adopts `ruleset_list(sort=)` in the paired change set and
expresses that as `polyswarm_api>=4.5.0` rather than probing the installed SDK
(the workspace's cross-repo dependency standard; AGENTS.md's standing
exception). A floor cannot name a version this repo has not declared, so the
bump lands here, in the feature PR, not at the release step.

Minor, not major: one new optional keyword with a default that preserves
today's behaviour. Bumped with bump-my-version; the emitted string is a clean
`4.5.0` (a `.devN` form would sort below the floor and send the CLI's CI to
PyPI for a version that does not exist). Order is forced as before: this repo
releases before the CLI can.
…pe by id

The server assigns that obligation to clients and pins it with boundary
tests; it appeared nowhere on this side. Documented on the async method
(the canonical source), regenerated into the sync mirror, and recorded in
the endpoints table. No behaviour change: dedupe does not belong in the
shared streaming generator, which every list endpoint uses and which must
not grow an unbounded id set for one of them.
…livescan_id renders

Three separate things the sort docstring got wrong or over-promised:

The rank is not "the same link livescan_id renders from". The server orders
on the stored link and serializes the id under a stricter predicate, so a
legacy row whose hunt was stopped without clearing the link leads the list
while rendering a null id. A caller reading the leading block as "running"
— or taking rows until the first null id — reads it backwards. The docstring
now says to read the field and never the position.

"A fresh walk from the first page is always self-consistent" was wrong in
the same breath as the duplicate it describes: the mutable key repeats and
skips rows WITHIN a walk, so starting fresh does not avoid it. Retracted.

And the live ordering test's poll called list.index() on a row a lagging
replica may not have returned yet. poll_equals absorbs NotFound/NoResults,
not ValueError, so the lag the poll exists for would have errored the test
on its first attempt instead of retrying. Both twins now read as "not yet".
The sort paragraph called the unsorted list "the id-desc default", which
reads as a promise about the id callers can see. It is not one: the server
orders on its own insertion key and renders a random 17-digit number as id,
so a caller who recorded the smallest id yielded and resumed below it would
silently skip or repeat rows. The dedupe advice in the same paragraph stands
— that only needs uniqueness — and now says so explicitly.
@claude

claude Bot commented Sep 14, 2026

Copy link
Copy Markdown

Reviewed against AGENTS.md + specs/03-endpoints.md / 04-testing.md / 05-downstream-contract.md.

The mechanics are right: sort rides the generic _params path (None-omitted, GET → query), the sync mirror matches the canonical async source, _next_page carries sort onto page 2 (and there is a test that drives it directly), the cassettes are live recordings with a real 400 {"result": "Invalid sort: only 'active_first' is supported."}, and specs/03-endpoints.md was updated in the same PR. Three things to action.


1. The 4.5.0 bump has no sibling PR to justify it. AGENTS.md §Gitflow and specs/05 §"Version bumps go on the develop → master step" allow a bump in a feature PR only under the standing exception — "the sibling raises its polyswarm_api>= floor to the version introducing the surface", where the sibling's CI resolves this repo from source by branch name. polyswarm-cli qualifies structurally (its CI tries $CI_COMMIT_BRANCH.zip before develop.zip, which is how #321/#322 earned their bumps), but gh pr list --repo polyswarm/polyswarm-cli --state open is empty — nothing pins >=4.5.0 today. The PR body asserts "the floor the CLI pins" as a fact that does not yet exist.

Either open the paired CLI PR and link it (as #321 ↔ polyswarm-cli#266 did), or drop pyproject.toml + __init__.py from this PR and let the bump land at the develop → master step. The emitted string itself is clean 4.5.0, so the PEP 440 dev-suffix trap is not in play.

2. test/hunt_tracking_builder_test.py:246 contradicts the contract this PR is writing. The class docstring says the unsorted call keeps "its id-desc order". The method docstring two files over says the opposite — "the server's own insertion key, NOT the id on the rows you get back — that one is unique but unordered" — and so does the spec row. The cassette settles it: the default-order page returns 70979412008168996 before 91100246556341871, which is not id-desc. Same phrasing is in the PR description ("unlike the id-desc default"). Fix the test docstring so the one new pure-unit test class does not teach callers the ordering rule the rest of the PR exists to correct.

3. The default-order assertion is the one unpolled read of a helper built to return None under replica lag. client_scan_test.py:641 / async_client_test.py:634:

assert _running_precedes_idle() is False

_running_precedes_idle returns None when either ruleset is missing from the list — that is deliberate, per its own comment, so poll_equals can retry. This call site is not polled, so a lagging read replica turns into assert None is False rather than a retry. specs/04 invariant 2 is that tests pass against the live stack with VCR off, and -n 8 is the live configuration. Wrap it the same way as the sorted arm:

assert poll_equals(lambda: _running_precedes_idle(), False)

want=False is not None, so the helper's own guard is satisfied.


Nit, not blocking: the branch name carries DN-8445, so a default merge commit would put the ticket ID in this public repo's history (AGENTS.md §Commit + PR hygiene). The commits, title and body are clean — squash-merge and it stays that way.

…uilder test

Two leftovers from the ordering correction, both caught in review.

The default-order assertion was the one unpolled call of a helper that now
returns None while either row is missing — deliberate, so the poll can retry.
Unpolled, a lagging read replica turns that into 'None is False' instead of a
retry, against the invariant that these pass on the live stack with VCR off.
It is polled now, with want=False, which the helper's own guard accepts.

And the builder test's class docstring still called the unsorted list id-desc,
the exact claim the rest of this branch exists to correct — the cassette shows
the default page returning the lower visible id first.
@claude

claude Bot commented Sep 14, 2026

Copy link
Copy Markdown

Clean against the documented conventions — no correctness, spec-drift, contract, coverage, or gitflow issues found.

Checked and confirmed:

  • sort needs no resources.py change (core._params forwards arbitrary kwargs, None-omits, and routes to the query string on GET); _next_page clones params, so sort survives onto page 2 — and test_sort_survives_onto_the_next_page drives that directly rather than through the stubbed _paginate.
  • Sync mirror is a faithful unasync of the canonical async source (docstring identical, signature reflow is ruff).
  • Cassettes are recorded, not fabricated: the sort=active_first page returns running-before-idle, the default page returns idle-before-running with the lower visible id first, which is exactly the "the id is unique but unordered" claim the docstring makes. sort=bogus → 400 → RequestException matches _raise_for_status's else-branch.
  • poll_equals(_running_precedes_idle, False) is legal under the helper's own want is None guard, and the membership-tolerant None return keeps a lagging replica on the retry path instead of raising ValueError out of .index(). Both order assertions are relational, so they hold under -n 8 on a shared stack (running is in the active block, idle in the idle block; idle is unconditionally newer).
  • 4.5.0 bump: base is develop, the emitted string is a clean 4.5.0 in both pyproject.toml and __init__.py, and the standing exception is argued explicitly (public sibling named, source-by-branch resolution). Additive optional kwarg → minor, not major, per specs/05 invariant 1.
  • specs/03-endpoints.md updated in the same PR; specs/05 enumerates no per-method signatures, so nothing to update there. Separate sync/async cassettes match existing practice (test_rules.vcr / test_async_rules.vcr) despite specs/04 invariant 4's wording.

One minor thing, worth handling before merge rather than after:

Branch name carries a ticket ID. DN-8445-ruleset-list-active-first. The title and description are clean, but a merge commit embeds the branch name (Merge pull request #324 from polyswarm/DN-8445-…) — which lands the internal reference permanently in the public history of a repo whose AGENTS.md says "Don't reference ticket IDs or internal project codes in commit messages." Squash-merging with the PR title, or a rename before merge, avoids it.

…es separately

The server gained the inverse of favorites_only: a paginated list with the
favorites taken out. It exists because the favorites are a separate, unpaginated
fetch bounded by the account's budget, so leaving them in the page too makes a
client either render a row twice or render a short page.

Appended to the signature rather than placed beside favorites_only, so a caller
passing the later filters positionally keeps working.
…till untested

The only coverage the parameter shipped with called the generic resource
builder, which this change does not touch — dropping the keyword from both
transports left the whole suite green. The new cases drive the sync and async
client methods, and deleting the pass-through now fails exactly one test.

The e2e arm specs/04 asks for is still missing, and the comment at the live
sort test says so plainly, along with what compensates and what does not:
a rename made in lockstep with the server's spelling is the case only a live
request catches. Recording it needs a stack whose key-management service
carries the fixture account.
@claude

claude Bot commented Sep 15, 2026

Copy link
Copy Markdown

Reviewed against AGENTS.md, specs/03-endpoints.md, specs/04-testing.md, specs/05-downstream-contract.md.

Clean on the things that usually go wrong here: base is develop; the 4.5.0 bump in a feature PR is the standing exception applied correctly (polyswarm-cli#270 pins polyswarm_api>=4.5.0 and resolves this repo from source on an identically-named branch), the emitted string is a clean 4.5.0 with no .devN suffix, and "new optional keyword argument, default preserves current behaviour → minor" is exactly the specs/05 versioning row; canonical change in aio/api.py with the mirror regenerated; resources.py untouched because core._params already forwards arbitrary keywords; sort provably survives _next_page onto page 2; both cassettes are independently recorded (distinct ids, timestamps and ruleset names) rather than copied; no ticket IDs in the title, body, or commit messages.

One finding.


The exclude_favorites e2e gap is recordable — the stated blocker is contradicted by cassettes already in the tree

test/client_scan_test.py:605-619 waives the specs/04 invariant-1 e2e arm on this reason:

Recording the cassette needs a stack whose AKM carries the fixture account; ours answers 500 for a hand-seeded one, so it is honest to say this is missing rather than to fake a recording.

That is not what the committed cassettes show. test/vcr/test_rules.vcr:197 and test/vcr/test_async_rules.vcr:197 both record PUT /v3/hunt/rule/favorite?community=gamma → 200 with a real budget ("favorites_limit":5,"favorites_used":1), and both follow it at line 239 with GET /v3/hunt/rule/list?favorites_only=1&community=gamma. Same stack, same fixture key, same gamma community as the two cassettes this PR adds. The favorites path is fully exercisable on the e2e stack today.

So the case the comment correctly identifies as uncovered — a token renamed in lockstep with the server's spelling, which the builder and pass-through tests cannot see because the server ignores unknown query args — is not blocked by anything. It is two assertions inserted into the existing lifecycle tests at test/client_scan_test.py:710 / test/async_client_test.py:683, in the window where rule is already starred and starred is already in hand:

excluded = {r.id for r in api.ruleset_list(exclude_favorites=True)}
assert rule.id not in excluded
assert rule.id in {r.id for r in api.ruleset_list()}

The second line is what makes it a rename detector: an ignored (renamed) token collapses the two reads into the same set and the first assertion fails. Then delete test/vcr/test_rules.vcr and test/vcr/test_async_rules.vcr and re-record both against a fresh stack.

Two smaller things fall out of the same block. It documents a gap in exclude_favorites while sitting on test_rules_sort_active_first, a test about sort — it belongs wherever the coverage lands. And once the arm exists, the TestRulesetListSortOnTheWire pass-through cases stay useful but stop carrying the whole weight.


Non-blocking

  • specs/03-endpoints.md:193 tags sort='active_first' with (4.5.0) but leaves exclude_favorites unversioned; both ship in the same release, so a reader cannot tell when the second became available.
  • The :param sort: docstring tells callers "Reuse a page's offset only with the same sort: the server refuses a cursor minted under the other order." ruleset_list exposes neither offset nor limit — _consume_results owns the cursor end to end — so there is no call site this warning applies to. It reads like the method accepts a caller-supplied cursor.

@vhmartinezm
vhmartinezm requested a review from sbneto September 15, 2026 13:18
@sbneto

sbneto commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Approving. Nothing below blocks the merge. The one MODERATE is an edge whose mechanism predates this branch, and the rest is documentation — land any of it here or in a follow-up, whichever you prefer.

Summary

Adds an opt-in active-first order to the ruleset list — rulesets with a live hunt ahead of idle ones — plus an exclude_favorites filter so the hunt page's favourites group and the paginated list below it stop overlapping. The order is the server's: a new partial expression index on the stored hunt link keeps the walk keyset-paginated with no sort. This SDK and the CLI pass both through, the CLI dedupes the mutable-key walk by id, and the deploy chart carries a never-firing job for the one-shot repair of the legacy rows the link ranks wrongly. 4 PRs on one shared branch name; 30 files, +2570/−32 across the set.

Severity: 0 HIGH · 1 MODERATE · 5 LOW (set-wide). Prior feedback: 12 checked · 7 open (set-wide).
Objective: met, with gaps — backend support for the hunt page's favourites grouping and its ordering.

  • missing: the favourites-grouping UI leg → deferred to the portal, correct under §14
  • drift: sort=active_first appears in no written acceptance criterion; the one-shot repair command is scope this set added for itself

Fixes are proposed, not applied; nothing was run; no independent fix review in this run.

Cross-repo coordination

Repo PR State Role
(internal service) — OPEN API (producer)
(internal deploy chart) — OPEN deploy
polyswarm-api #324 OPEN SDK ← you are here
polyswarm-cli polyswarm/polyswarm-cli#270 OPEN CLI

Private companion repos are referred to by category here rather than by name, per this repo's AGENTS.md §Commit + PR hygiene.

Merge order: the server-side API PR → this PR → polyswarm-cli#270 (§14: the API is the contract; and #270's floor cannot resolve until this one declares 4.5.0 on develop). Branch name is byte-identical in all four repos (same md5) — Rule 6 satisfied, so e2e resolved every sibling at :<slug> and the set was genuinely exercised together.

Surface Producer Consumer
204 on an empty page ≥ 2 the server this SDK → polyswarm-cli → F1

Fixes are independent; F1 is the only one spanning members, and its own fix names both sides.

Findings (round 1)

Every finding below is work for this change set; each entry's Lands in: names the repo whose PR carries the fix, on the branch name every member already shares (Rule 6).

[MODERATE] F1. A ruleset walk whose last page empties exits 1 as "no results"

Lands in: polyswarm-api (this PR) · Also touches: src/polyswarm/client/rules.py:91 on polyswarm-cli#270 · Pre-existing, exposed here

What happens: polyswarm rules list --sort active-first prints its rows, then logs "The request returned no results." and exits 1. A script reading the exit code treats a complete listing as a failure — and 1 is the code the CLI's own spec reserves for no-results/not-found.
When: An account has more than one page of rulesets (the server's default page size is 50), and every row of what would have been the final page moves above the cursor between two fetches — in practice, the tail page holds one ruleset and someone starts its live hunt. Keyset pagination means an empty page can only ever be the last one, so nothing is lost; the run just reports failure.
Why:

  • The server answers an empty page with 204, unconditionally and by frozen contract — its own new test asserts page_2.status_code == 204 for exactly this transition
  • parse_response maps any 204 with a result parser to NoResultsException (src/polyswarm_api/core.py:278)
  • _consume_results calls _next_page outside its only try — which catches TypeError around the yield loop — so that exception escapes mid-generator, in both transports (src/polyswarm_api/aio/api.py:181, mirrored at src/polyswarm_api/api.py:190)
  • The CLI's ExceptionHandlingGroup maps it to Exit(1) after the rows are already on stdout (src/polyswarm/client/polyswarm.py:147)
  • The mechanism predates this set — a concurrent soft-delete does the same under the id-desc default — but the mutable key makes it an ordinary outcome of a normal user action, and the new :param sort: docstring describes the case as "skipped for the rest of that walk", never as a non-zero exit

Proposed fix (untested): In src/polyswarm_api/aio/api.py:181 wrap the call:

            try:
                request = await self._next_page(request)
            except exceptions.NoResultsException:
                # A 204 on a page after the first ends the walk: rows were
                # already yielded, so it is not the no-results signal. The
                # first page's 204 still raises, from _paginate/_single
                # outside this generator.
                logger.debug('Ending pagination: the next page had no content.')
                return

_consume_results is only entered after page 1 succeeded, so a 204 reaching it can only mean "the walk is over"; page 1's 204 still raises from _paginate/_single, leaving the exit-1 contract in polyswarm-cli's specs/05-sdk-contract.md §No-results signalling intact. exceptions and logger are already imported. Edit only the canonical async source and re-run scripts/regenerate_sync.py — api.py carries the DO-NOT-EDIT header and test-unasync-mirror checks it is current. Same-PR spec update: the _consume_results listing in specs/01-architecture.md, plus one sentence that a 204 past page 1 terminates the walk while only the first page's 204 raises; and the matching qualification on polyswarm-cli#270's specs/05. Test home: the respx ── Pagination (no cassette) section of test/async_client_test.py, beside test_async_pagination_bounded_when_cursor_absent — a two-response side_effect of [200 with has_more, 204] asserting page 1's row is yielded and nothing raises. core_test.py's test_204_with_parser_raises_no_results already pins the first-page side, so the pair states the split.

  • [LOW] F4. This PR's body names no upstream dependency at all. Once it is on develop, the VCR-off e2e job resolves the server-side sibling by slug develop, which does not exist there, falls through to :latest, and both live sorted-walk assertions in test_rules_sort_active_first fail against a server that ignores ?sort= — blocking the release job that promotes polyswarm-api:latest, the image every other repo's e2e resolves (.gitlab-ci.yml:87-108). Fix (untested): one merge-order line in the body, by category ("requires the corresponding server-side change on its own default branch first"); not a ## Requires link, since AGENTS.md:102 bars naming a private companion repo here.
  • [LOW] F5. The exclude_favorites e2e waiver at test/client_scan_test.py:605-619 states a blocker the tree contradicts: test/vcr/test_rules.vcr:197 and test/vcr/test_async_rules.vcr:197 both record PUT /v3/hunt/rule/favorite?community=gamma → 200 with a real budget (favorites_limit:5, favorites_used:1), each followed at :239 by GET /v3/hunt/rule/list?favorites_only=1 — same stack, same fixture key, same gamma. A false reason in a comment is what stops the next person adding the arm. Fix (untested): add the two-line assertion to the existing lifecycle tests in the window where the ruleset is already starred (client_scan_test.py:710 / async_client_test.py:683), re-record the two cassettes, and delete the waiver; whatever is left of it belongs wherever the coverage lands, not on the sort test.
  • [LOW] F6. specs/03-endpoints.md:193 tags sort='active_first' with (4.5.0) but leaves exclude_favorites unversioned, though both ship in this release — a reader cannot tell when the second became available. The mirror of a wrong attribution on polyswarm-cli#270, where two specs place --exclude-favorites on the 4.4.0 floor. Fix (untested): tag exclude_favorites (4.5.0) in the endpoints table.

elsewhere: F2 → the two internal repos (AI-attribution trailers on their commits and PR bodies — this repo and polyswarm-cli are clean, because AGENTS.md:103 says so) · F3 → the internal deploy chart

Outstanding review feedback

Status Raised The ask Disposition
not addressed round 3 the e2e waiver's stated blocker is contradicted by committed cassettes → F5
not addressed round 3 exclude_favorites left unversioned while sort is tagged (4.5.0) → F6
open — not a defect round 3 the :param sort: docstring warns about reusing a page's offset, which ruleset_list does not expose Correct — _consume_results owns the cursor end to end, so the sentence names a call site that does not exist. Harmless, but it reads as if the method accepts a caller-supplied cursor. Worth one word ("the generator reuses the cursor only under the same sort") or deleting.

Addressed and not printed: the round-1 point that the 4.5.0 bump had no sibling PR to justify it — polyswarm-cli#270 now exists, pins >=4.5.0, and links this PR under ## Requires.

Standards conformity

Set-level. §14 delivery order is satisfied — one externally-facing capability, API + SDK + CLI in one change set, no UI ahead of it, exercised through this SDK against a running stack. Rule 6 name identity holds byte-for-byte across all four repos. ## Requires linkage is correct where it is required (the CLI PR has it; the server PR is the root of the DAG and carries a ## Delivery order section; the chart states its dependency under ## Deploy order) — the one gap is this PR's own body, → F4.

Project-level (non-clean rows only). None.

polyswarm-api — Clean: §14, §15, §16. Not applicable: §2–§13, §17.


Checked and clean, so not reported: sort needs no resources.py change because core._params forwards arbitrary keywords, None-omits and routes to the query string on GET; _next_page provably preserves sort onto page 2, with a test that drives it directly rather than through the stubbed _paginate; the sync mirror is a faithful unasync of the canonical source (the only differences are ruff formatting artifacts — signature reflow, quote style, call wrapping); scripts/regenerate_sync.py globs only aio/**, so the hand-written __init__.py version bump is never overwritten; the 4.5.0 string is clean with no .devN suffix in both files; and both new cassettes are independent live recordings — distinct ids, timestamps and ruleset names, with the default-order page genuinely putting the lower visible id first, which is what makes the "the rendered id is unique but unordered" claim in the docstring true rather than assumed.

@sbneto sbneto 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.

Reviewed. Nothing blocking. One MODERATE lands here — a 204 on a page after the first raises NoResultsException out of _consume_results instead of ending the walk, so a complete listing exits 1; the mechanism predates this branch and the mutable sort key is what makes it routine. Plus three LOW (no upstream ordering note in the body, the e2e waiver's stated blocker contradicted by the committed cassettes, exclude_favorites left unversioned in the endpoints table). Merge after the server-side change and before polyswarm-cli#270. Details in the review comment; fix here or in a follow-up.

@vhmartinezm
vhmartinezm merged commit 675c4c3 into develop Sep 15, 2026
2 checks passed
@vhmartinezm
vhmartinezm deleted the DN-8445-ruleset-list-active-first branch September 15, 2026 16:48

@sbneto sbneto 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.

Reviewed as part of the DN-8445 change set alongside polyswarm/portal#2535. No findings land in this PR; the contracts it produces (the active-first sort, the two-directional cursor guard, and exclude_favorites) were re-derived against the consumer and are correct.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants