Skip to content

feat(rules): list --sort active-first - #270

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

polyswarm rules list --sort active-first — surfaces the API's opt-in active-first ruleset
order, where rulesets carrying a live hunt link list first. Without the flag nothing changes.

The flag maps to the SDK's ruleset_list(sort='active_first'); the CLI adds no ordering of
its own, because the list is keyset-paginated and a client-side sort would reorder one page
and misrepresent the rest.

The help text is explicit that the rank is the stored link rather than what Live Hunt Id
renders from: a legacy row stopped while still linked leads the list and renders no Live Hunt
Id at all, reading exactly like an idle one. Read the field, never the position.

The dedupe

This command walks every page and exposes no limit, which makes it the consumer the SDK puts
the dedupe obligation on. The active-first key is the live-hunt link itself, so a ruleset
whose hunt stops between two page fetches drops below the cursor and the server serves it
again — the run printed it twice, and a script counting the output double-counted it. Rows
are now emitted at most once per run, keyed on id, unconditionally: the id is unique under
either order, and a gate would be a second place to update when another mutable order appears.

That leaves two costs. Neither is hidden: both are in the --sort help, the command
docstring, and specs/05 §"A mutable order makes the walk the caller's problem".

  • The surviving copy is the stale one. First-wins is the only option for a streaming
    printer, and that copy carries the values from before the transition — so the one row the
    dedupe acts on prints the Live Hunt Id of a hunt that has already stopped. Under this order
    a moved row is authoritative in neither its position nor its fields; a fresh run shows the
    settled state.
  • A row whose hunt STARTS mid-walk moves above the cursor, so it never reaches this client
    at all. Nothing client-side can repair that. A re-run lists it.

Follow-up for the release PR

The dedupe is unconditional, so plain polyswarm rules list changes behaviour too. This repo has no changelog, so per specs/05 §"Behaviour changes to existing invocations" that belongs on the develop → master PR at cutoff, not here.

--exclude-favorites

The inverse of --favorites-only, which the server refuses to combine with it. For a client that
lists the favorites separately. A False flag is not a filter here either, so an unflagged
invocation sends nothing new.

Requires

This PR raises the floor to polyswarm_api>=4.5.0. Merge precondition: the SDK's develop
must DECLARE 4.5.0, clean of any .devN suffix, or CI reinstalls the SDK from the package
index over the archive build. Until then CI resolves it from source — it installs
$POLYSWARM_API_ARCHIVE/$CI_COMMIT_BRANCH.zip, and that branch is pushed in the SDK repo
under this identical name.

Verification

Full suite green (195). Four cases hold the dedupe, each failing against a different wrong implementation: a row served twice prints once; the default order is not narrowed by it; two distinct rulesets sharing a name both render (pins the key as the id — names are not unique); and the surviving copy is the first one, stale values and all (pins first-wins, so a last-wins rewrite fails). The duplicate the tests use differs in the field the sort ranks on, which is the shape a real re-serve takes.

Forwards `ruleset_list(sort='active_first')` — the hunt page's order, 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. Server-side
like the filters: the list is keyset-paginated, so a local sort would only ever
reorder one page. The option is a closed click.Choice (hyphenated CLI spelling,
underscored server token) and is forwarded only when given, so the unsorted
default request is unchanged. The autospec tests double as the signature check
against the installed SDK, alone and combined with the filters.
…ort=)

The floor names the SDK version that adds the keyword `rules list --sort`
forwards (specs/05 §Current floor follows the pin). Mergeable once the SDK's
develop declares 4.5.0; releasable once that version is on PyPI — the SDK
releases first. CI resolves the SDK from source by branch name, falling back
to develop, so the paired SDK branch must carry the identical name.
…n Live Hunt Id

The help text said the order came from "the same one Live Hunt Id renders
from". The server ranks on the raw link and renders the id under a stricter
predicate, so a legacy row whose hunt was stopped without clearing the link
leads the list with an empty Live Hunt Id. Someone reading the list top-down
for what is running would have stopped at that row.
…y Live Hunt Id

`rules list` walks every page and exposes no limit, which makes it exactly the
consumer the SDK puts the dedupe obligation on. Under --sort active-first the
ordering key is the live-hunt link, so a ruleset whose hunt stops between two
page fetches drops below the cursor and the server serves it again: the run
printed it twice and any script counting the output double-counted it. Rows are
now emitted at most once per run. The dedupe is unconditional — the id is unique
under either order and one set of ids costs nothing next to the rendered rows.

The symmetric case cannot be repaired from here and is documented rather than
hidden: a hunt STARTED mid-walk moves its row above the cursor and it never
reaches this client until the next run.

Two doc corrections in the same push. The --sort help promised that a stale-link
row "leads the list with an empty Live Hunt Id", but the formatter gates the
whole Live Hunt Id pair on a truthy value, so such a row prints no such line at
all and reads as idle. And the commands spec called that row
"stopped-but-unlinked", the inverse of the state it means: it is stopped and
STILL linked, which is why it leads a list ranked on the link being present.
@claude

claude Bot commented Sep 14, 2026

Copy link
Copy Markdown

Review — against AGENTS.md + specs/02, 04, 05

Code is correct as far as I can check it: base is develop, the CLI version is untouched (only the dependency floor moves, per §Gitflow), sort is forwarded only when given so the default request shape is unchanged, the dedupe is keyed on a value that is unique under either order, and the --sort help text matches what formatters/text.py:337 actually does (Live Hunt Id is gated on a truthy livescan_id, so a stale-linked row renders no line at all — the help and the specs/02 row now say that correctly). Two things need action.

1. Missing ## Requires section (spec violation). AGENTS.md §Commit + PR hygiene: "PRs that depend on an unreleased polyswarm-api surface must link the SDK PR under a ## Requires section", and specs/05 §Invariants repeats it ("the CLI PR links it under ## Requires"). The body has a ## Dependency prose section naming >=4.5.0 but no link to the SDK PR. Rename the heading to ## Requires and link the polyswarm-api PR — that link is the thing a reviewer uses to confirm the merge precondition in specs/05 §Current floor (SDK develop must declare 4.5.0, clean of any .devN suffix, before this can merge, or CI silently reinstalls the SDK from PyPI over the archive build).

2. Test gap: the dedupe key is not pinned as the id. test_a_row_served_twice_by_the_mutable_sort_is_printed_once repeats id=5 and name=stops-mid-walk, and test_the_default_order_is_not_narrowed_by_the_dedupe uses rows that differ in both id and name. An implementation that deduped on name (or on the whole rendered block) passes both, so neither test observes the property the docstring and specs/02 claim ("keyed on id"). The missing case is the one that separates them: two distinct rulesets sharing a name (_ruleset(id=5, name=dup), _ruleset(id=7, name=dup)) must both reach the output — assert count(...) == 2. That is also the case a real inventory produces, since ruleset names are not unique.

Nothing else: commit messages are clean of ticket refs (the DN- prefix is on the branch only, which AGENTS.md explicitly permits — just squash-merge with an explicit subject so it never lands in history), and specs/02 / specs/05 were updated in the same PR as required.

The two dedupe tests repeated the same row wholesale and compared rows that
differed in every field, so an implementation keyed on the name — or on the
rendered block — passed both. Ruleset names are not unique, so that variant
would swallow a real row from any inventory listing. The new case is the one
that separates them: two distinct rulesets sharing a name must both render.
Re-keying the dedupe on the name fails it and nothing else.
@claude

claude Bot commented Sep 14, 2026

Copy link
Copy Markdown

Reviewed against AGENTS.md, specs/02-commands.md, specs/04-testing.md, specs/05-sdk-contract.md. The code itself is sound — kwargs forwarding is correct and only-when-given, the sort is server-side per the keyset-pagination rule, the floor bump follows the documented procedure, base is develop, and the CLI's own version is untouched. Four things worth acting on, none blocking the implementation.

1. The dedupe keeps the stale copy — and that is the copy whose Live Hunt Id the help text tells you to trust (src/polyswarm/client/rules.py:76-85)

The scenario the dedupe exists for is a hunt that stops mid-walk. Page 1 renders that ruleset while it is still linked — Live Hunt Id: X. The row then drops below the cursor and is re-served, now with livescan_id cleared. First-wins drops the second copy, so the run prints Live Hunt Id: X for a hunt that is no longer running.

That is unavoidable for a streaming printer (the first copy is already on stdout), so this is a docs fix rather than a code one — but it undercuts the escape hatch the --sort help offers. The help says the position is unreliable and to "Read the field, never the position" (rules.py:41-48); in exactly the case the dedupe handles, the field is unreliable too. Either say so in the docstring, or soften "never the position" so it stops promising the field is authoritative under a mid-walk transition.

2. The dedupe test pins the wrong duplicate shape (tests/formatter_hunt_fields_test.py:207-209)

repeated repeats the row wholesale. The real re-serve differs in the field the sort ranks on — that is why it moved. Make the second copy _ruleset(id='5', name='stops-mid-walk', livescan_id=None) against a first copy carrying a livescan_id, and assert which one rendered. That pins first-wins as a decision instead of leaving it as whatever the loop happens to do, and it is the case that catches a future last-wins rewrite. test_two_rulesets_sharing_a_name_both_render already did this work for the key; this does it for the value.

3. The dedupe rule is documented only in the command catalogue (specs/02-commands.md:33)

"The obligation the SDK puts on a multi-page consumer of this mutable key" is an SDK-consumption invariant, and specs/05-sdk-contract.md §Consuming the SDK correctly is where those live — it already carries "Endpoint methods return generators — iterate them" and §No-results signalling. The next command that walks every page of a mutably-ordered list endpoint will read that section, not a parenthetical in a rules table row. The 05 diff here only raises the floor for the sort= keyword. Add a short subsection there and let 02 point at it.

4. ## Dependency should be ## Requires

AGENTS.md §Commit + PR hygiene: "PRs that depend on an unreleased polyswarm-api surface must link the SDK PR under a ## Requires section"; specs/05-sdk-contract.md §Coordinated changes step 2 repeats it. The link is there and the ordering described is right — only the heading is off, and specs/99-open-questions.md:29 notes this convention is review-enforced rather than CI-enforced, so the literal heading is the whole signal.

One item for the release step, not for this PR: the dedupe is unconditional, so plain polyswarm rules list changes behaviour too. specs/05-sdk-contract.md §"Behaviour changes to existing invocations" puts those on the develop → master PR, since this repo has no CHANGELOG — worth carrying over at cutoff.

Unverifiable from here, worth confirming before merge: that the SDK's develop declares 4.5.0 with no dev suffix, read off the branch tree rather than the last tag (specs/05-sdk-contract.md:80).

…y so

Round two of review. The dedupe keeps the first copy — the only choice a
streaming printer has, since that copy is already on stdout when the second
arrives — and the first copy carries the values from BEFORE the transition. So
the one row the dedupe acts on prints the Live Hunt Id of a hunt that has
already stopped. The help said to read the field rather than the position;
under a mid-walk move neither is authoritative, and the help, the command
docstring and the commands catalogue now say that instead of promising it.

The dedupe test repeated a row wholesale, which is not the shape a re-serve
takes: the two copies differ in exactly the field the sort ranks on, because
that is why the row moved. It uses that shape now, and a new case pins
first-wins as a decision — a last-wins rewrite fails it and nothing else.

The invariant itself moved to specs/05 §Consuming the SDK correctly, beside
the other SDK-consumption rules, where the next command that walks a mutably
ordered endpoint will find it; the commands catalogue points at it.
@claude

claude Bot commented Sep 14, 2026

Copy link
Copy Markdown

Reviewed against AGENTS.md + specs/02, 04, 05. Clean on correctness, spec drift, the floor bump, and test coverage — the dedupe tests are discriminating (name-keyed and last-wins implementations both die), and specs/05 §"A mutable order makes the walk the caller's problem" matches the code it describes. Two minor items:

1. tests/formatter_hunt_fields_test.py — class/module docstrings not updated with the tests. RulesListZeroArgTest still says "rules list calls a zero-argument ruleset_list()", but the class now carries seven tests about sort forwarding and mid-walk dedupe, none of which are about the zero-arg case. The module docstring's "Pins two contracts" enumeration likewise stops at the 4.4.0 filters. specs/04 leans on these files being self-describing for the coverage matrix it says is still to come; a reader landing on the class name will not expect the dedupe pins to be there. Either widen both docstrings or split the sort/dedupe tests into their own class.

2. Merge subject — the branch name must not land. AGENTS.md allows the ticket-prefixed branch name here precisely because this is a paired change (CI resolves the SDK by $CI_COMMIT_BRANCH), but only on the condition that it is contained at the merge: "Squash-merge with an explicit clean subject, and it never lands." The last merge on develop did not — 44a50e0 reads Merge pull request #267 from polyswarm/DN-8378-yara-matched-strings, a ticket ID now in public history. Worth being deliberate about it on this one rather than taking the gh default.

Non-blocking observation: the --sort help text and the specs/02 table cell restate the same ~150 words about stale Live Hunt Id values three times over (help, specs/02, specs/05). The help is the one that has to be there — the user reading --help is the one who acts on a stale field, as specs/05 says — but the specs/02 cell is now a paragraph inside a table row, and it is the copy most likely to drift. Consider trimming it to the one-line claim plus the existing pointer to specs/05.

…e duplicated prose

Seven tests about sort forwarding and mid-walk dedupe had accumulated inside a
class whose docstring promised only the zero-argument call, so nobody landing
on the name would expect the dedupe pins to be there. They are their own class
now, with a docstring that states both contracts it holds; the module docstring
enumerates the new one alongside the others.

The same ~150 words about stale Live Hunt Id values were restated in the help,
the commands catalogue and the SDK-contract spec. The help keeps them — the
user reading --help is the one who acts on a stale field — and the catalogue
cell, the copy most likely to drift, is back to the one-line claim plus its
existing pointer.
@claude

claude Bot commented Sep 14, 2026

Copy link
Copy Markdown

Reviewed against AGENTS.md, specs/02-commands.md, specs/04-testing.md, specs/05-sdk-contract.md.

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

Checks that mattered, for the record:

  • Correctness. sort.replace(-, _) is fed by a closed click.Choice, so no arbitrary token reaches the server; the kwarg is dropped by the same is not None filter as the flags, so the unsorted request is unchanged. The dedupe sits in the streaming loop, keys on ruleset.id, and is inert under the id-desc default (immutable cursor key ⇒ no re-serve). --status active filters on a mutable field but the cursor is still the id, so that path loses rows rather than repeating them — the unconditional dedupe can't swallow anything there either.
  • Spec. specs/05 §"A mutable order makes the walk the caller's problem" is the right home for the invariant (beside the generator-consumption rules), §Current floor follows the pin as that section requires, and specs/02 points at it rather than restating it. The "say it in the command's help too" clause is honoured by the --sort help + docstring.
  • Downstream/pin. polyswarm_api>=4.5.0 with ## Requires linking the SDK PR, and the merge/release preconditions stated the way §Version pin splits them. CLI's own version untouched — correct for a feature PR. The behaviour change to plain rules list is deferred to the develop → master PR, which is where §"Behaviour changes to existing invocations" puts it.
  • Tests. Style 1 throughout, autospec'd at polyswarm_api.api.PolyswarmAPI.ruleset_list. The four dedupe cases each kill a distinct wrong implementation (no dedupe / keyed on name / last-wins / gated-on-sort-and-over-broad), and the duplicate differs in exactly the ranked field, which is the real re-serve shape. No cassette needed — nothing here is wire-shape.
  • Gitflow. Base is develop; ticket prefix is confined to the branch name, which AGENTS.md explicitly allows as the CI coordinating key. Squash-merge with a clean subject so it stays out of history.

One nit, in the PR description only (not the code): the "The dedupe" section has a garbled duplicate — Two costs, both documented in the help, the command docstring and specs/05 §A mutable order makes the walk the caller's problem rather than hidden: reads as two merged sentences, and the STARTED mid-walk moves its row above the cursor… fragment appears twice. Worth tidying before merge.

The inverse of --favorites-only, which the server refuses to combine with it.
It exists for a client that renders the favorites as their own list: leaving
them in the paginated list too makes a page repeat a row or come back short.
A False flag is not a filter here either, so an unflagged invocation sends
nothing new.
@claude

claude Bot commented Sep 15, 2026

Copy link
Copy Markdown

Review — dedupe design, --sort forwarding, and the spec/help writeup all hold up. Four things.

1. --favorites-only --exclude-favorites has no client-side guard (correctness)

client/rules.py:38-42 — the help says the pair is "refused together", but nothing refuses it here, so polyswarm rules list --favorites-only --exclude-favorites makes the round trip and surfaces whatever the server 400 becomes. Which exit code that is is not obvious: ExceptionHandlingGroup (client/polyswarm.py:139-174) sends a PolyswarmException to 2, but anything rooted at httpx HTTPError to 1 — the code specs/02-commands.md reserves for no-results/not-found. A scripted caller cannot distinguish "you passed a contradictory pair" from "empty list".

This repo already has the pattern for exactly this: client/polyswarm.py:84 (UsageError, env shortcuts) and client/scan.py:52 (BadArgumentUsage, preprocessing flags). And this PR own test_sort_rejects_an_unknown_order makes the argument — "the CLI should not have to make the round trip to say so". Same applies.

Also the docstring opener, List rulesets, optionally filtered. All filters are conjunctive., is no longer true for that pair.

2. Floor attribution — --exclude-favorites is 4.5.0, not 4.4.0 (spec drift)

specs/02-commands.md lists --exclude-favorites among the filters and then says "rules favorite and the rules list filters need SDK 4.4.0 and rules list --sort needs 4.5.0". But exclude_favorites ships in polyswarm/polyswarm-api#324 (its own exclude_favorites section) — i.e. 4.5.0, same as sort=. specs/05-sdk-contract.md §Current floor names only ruleset_list(sort=...) as what moved the floor, and this PR ## Requires line describes #324 as ruleset_list(sort=) only.

The pinned value is right; the attribution is wrong in three places. Against a 4.4.0 SDK --exclude-favorites fails too, and the specs currently say it would not.

3. Missing test case

No coverage for the refused pair — specs/04-testing.md §What to test for a new command, item 3 (error paths map to the right exit code). If you add the guard per (1), the test is exit_code == 2 plus the message; if you deliberately leave it to the server, that decision still needs a pin, because the exit code it produces today is unverified.

4. Gitflow

Base develop, no CLI version bump, ## Requires present — all correct. Two merge-time notes: polyswarm/polyswarm-api#324 is still open, so per specs/05 §Version pin this cannot land until the SDK develop declares a suffix-free 4.5.0 (the body already says so); and the branch is ticket-prefixed, so squash-merge with an explicit subject per AGENTS.md §Commit + PR hygiene, or the ticket ID lands in public history via the default merge subject.

@vhmartinezm
vhmartinezm requested a review from sbneto September 15, 2026 13:19
@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 whose fix lands in the SDK, 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. The SDK and this CLI pass both through, this command 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 polyswarm/polyswarm-api#324 OPEN SDK
polyswarm-cli #270 OPEN CLI ← you are here

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 → polyswarm-api#324 → this PR (§14: the API is the contract). This is the one merge in the set that genuinely breaks if taken out of order — pyproject.toml:25 pins polyswarm_api>=4.5.0, and on develop CI would pull the SDK's develop.zip at 4.4.0 and then go to PyPI for a 4.5.0 that does not exist. Your ## Requires section and its merge-precondition paragraph already state exactly that, which is why it is not a finding. Branch name is byte-identical in all four repos (same md5) — Rule 6 satisfied.

Surface Producer Consumer
204 on an empty page ≥ 2 the server polyswarm-api → this 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#324 · Also touches: src/polyswarm/client/rules.py:91 (this PR — where it surfaces) · 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 specs/02-commands.md reserves 1 for no-results/not-found, so the code actively misreports.
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 no rows are lost; the run just reports failure.
Why:

  • The server answers an empty page with 204, by frozen contract — its own new test asserts page_2.status_code == 204 for exactly this transition
  • The SDK maps any 204 with a result parser to NoResultsException (polyswarm_api/core.py:278), and _consume_results calls _next_page outside its only try, so it escapes mid-generator
  • list_rules iterates api.ruleset_list(**kwargs) bare (src/polyswarm/client/rules.py:91) — no parallel_executor, so nothing absorbs it — and ExceptionHandlingGroup maps it to Exit(1) (src/polyswarm/client/polyswarm.py:142-147), after both formatters have already click.echo'd every row
  • This is the third outcome of a mid-walk move, and the one none of the new prose covers: the --sort help and the docstring say a row that starts a hunt "never reaches this client at all … A re-run lists it", and specs/05 §A mutable order… says it is "missing from that walk entirely". True — but when it was the last row, the walk also ends non-zero
  • The mechanism predates this set: a concurrent soft-delete empties the final page under the id-desc default too. The mutable key is what makes it an ordinary outcome of a normal user action

Proposed fix (untested): No code change here. The fix is in polyswarm-api#324 — wrap _next_page in aio/api.py:181 in try/except exceptions.NoResultsException: → return, since _consume_results is only entered after page 1 succeeded, so a 204 there means "the walk is over" while page 1's 204 still raises from _paginate/_single. In this PR the matching change is documentation, in the same set: qualify specs/05-sdk-contract.md:70 §No-results signalling (the first page raises; a later page ends the walk and the rows already printed stand) and add one line to your new §"A mutable order makes the walk the caller's problem" saying the empty-final-page case ends the walk cleanly, next to the "skipped" language already there.

  • [LOW] F6. Both specs place --exclude-favorites on the 4.4.0 SDK floor, but the keyword only exists in 4.5.0 — against a 4.4.0 SDK polyswarm rules list --exclude-favorites raises TypeError: ruleset_list() got an unexpected keyword argument before any request is made. specs/02-commands.md:33 lists it among "the server-side filters" and then says "the rules list filters need SDK 4.4.0 and rules list --sort needs 4.5.0"; specs/05-sdk-contract.md:128 names sort alone as what moved the floor, while its 4.4.0 paragraph still sweeps up "the ruleset_list filters". The pinned value is right — only the attribution is wrong, and specs/05 calls §Current floor "the one authoritative statement". Fix (untested): in specs/02-commands.md:33 write "rules favorite and the pre-existing rules list filters need SDK 4.4.0; rules list --sort and --exclude-favorites need 4.5.0", and make the 4.5.0 clause in specs/05-sdk-contract.md:128 name both keywords. The mirror of this is on polyswarm-api#324, where the endpoints table leaves exclude_favorites unversioned.

elsewhere: F4, F5 → polyswarm/polyswarm-api#324 · F2, F3 → the two internal repos (AI-attribution trailers on their commits and PR bodies — this repo and polyswarm-api are clean, because AGENTS.md:103 says so; and a §15 comment-duplication item in the deploy chart)

Outstanding review feedback

Status Raised The ask Disposition
not addressed round 5 --exclude-favorites attributed to the 4.4.0 floor, not 4.5.0 → F6
contradicted round 5 no client-side guard for --favorites-only --exclude-favorites; "the exit code may be 1" The exit-code premise does not hold, so the correctness argument falls with it: the server's 400 carries a JSON envelope, so _raise_for_status takes its else branch and raises RequestException — a PolyswarmException, which ExceptionHandlingGroup maps to Exit(2), the correct server-refusal bucket. It never reaches the httpx-HTTPError→1 arm. No defect; the round trip buys a clear server message. What does stand is one sentence: the docstring opener "All filters are conjunctive" is no longer true for that pair.
open — not a defect round 5 no test pins the refused pair's exit code Now verified by reading rather than by running: exit 2, with the server's "mutually exclusive" message. A pin would be cheap, but nothing is wrong today.
open — not a defect round 3 squash-merge with an explicit clean subject so the ticket-prefixed branch name does not land in public history A merge-time instruction for whoever presses the button, not a change to the diff. Worth repeating here because it is the one step that cannot be fixed after the fact.

Addressed and not printed: the missing ## Requires section, the dedupe-key test gap, the stale-surviving-copy documentation, and the test class/module docstrings.

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 the 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 — this PR is the one that needed it and has it; the one gap is on polyswarm-api#324's own body (→ F4, there).

Project-level (non-clean rows only).

Rule Evidence
§16 ◐ "Every in-house library surface a consumer uses is covered by the declared floor" holds — pyproject.toml:25 pins >=4.5.0, and it is the only SDK pin in the repo. Which surface moved it is documented wrongly → F6.

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


Checked and clean, so not reported: sort.replace('-','_') is fed by a closed click.Choice, so no arbitrary token reaches the server, and the kwarg is dropped by the same is not None filter as the flags, leaving the unsorted request byte-identical; the dedupe keys on a value that is unique under either order and is never None in any response the server can produce; and the four dedupe tests are discriminating — removing the dedupe fails the count assertion, a name-keyed one fails the shared-name test, and a last-wins rewrite fails the stale-copy test.

@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 LOW on this PR: both specs place --exclude-favorites on the 4.4.0 SDK floor, but the keyword only exists in 4.5.0 — the pin is right, the attribution is not. The set's one MODERATE surfaces in rules list (the walk exits 1 when its last page empties) but its fix lands in polyswarm-api#324; the matching change here is a doc qualification. Merge last, and squash with a clean subject so the branch name stays out of public history. Details in the review comment.

@vhmartinezm
vhmartinezm merged commit 5b67661 into develop Sep 15, 2026
2 checks passed
@vhmartinezm
vhmartinezm deleted the DN-8445-ruleset-list-active-first branch September 15, 2026 16:48
@claude claude Bot mentioned this pull request Sep 15, 2026

@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