Skip to content

feat: add metadata_raw_search for the raw metadata search endpoint - #331

Merged
sbneto merged 11 commits into
developfrom
DN-8634-metadata-raw-search
Oct 20, 2026
Merged

sbneto merged 11 commits into
developfrom
DN-8634-metadata-raw-search

Conversation

@sbneto

@sbneto sbneto commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

TL;DR

  • Adds metadata_raw_search(body, community=None) to both clients, for the new server-side POST /v3/search/metadata/raw.
  • The query body is passed through verbatim; the community rides the query string, not the body.
  • Returns one composite result — the engine's hits and aggregations together — not a generator.
  • 4 request-shape + 3 parse tests in the pure-unit tier, plus a live-e2e VCR lifecycle test per transport with freshly recorded cassettes.
  • No version bump (that belongs to the develop → master step).

The endpoint

The server runs a caller-authored OpenSearch query body against the metadata corpus and overlays the tenant filter itself. It accepts a closed grammar — anything it does not recognise is refused rather than passed through — and it is entitlement-gated and metered per query, including a query it refuses after running.

Callers get result.json['hits'] and result.json['aggregations'], plus result.json['partial'] when the server could not establish that the answer is complete — a list of every reason that fired (a timeout, a lost shard, a clamped bucket list). A partial marker is the only thing separating a truncated answer from a whole one — both are a 200 — so it is deliberately preserved rather than flattened away, and a test pins that.

Two contract details the docstring now states, because neither is guessable. Omitting size returns no documents — the server defaults it to 0, so an aggregation caller does not pay for hits they did not ask for.

And partial does not speak for the hit count. The server suppresses that signal when the body asked for no hits, because a total capped at track_total_hits is what every large query returns and reporting it would mark all of them partial. Exactness of a count is hits.total.relation — eq or gte — the engine's own field, passed through untouched. partial speaks for the aggregations and the search as a whole. Reading its absence as "this count is exact" would be wrong, which is why the docstring now separates the two.

Why a hand-written builder instead of the generic one

This is the one design choice worth a reviewer's time. On a POST, the generic builder puts every parameter into the JSON body. That cannot express ?community=…, and it is not merely inconvenient: the server reads community/communities out of the POST body when it resolves scope, while its grammar rejects those keys. A community folded into the body would therefore select the scope via a key the server then refuses — scope chosen by one layer and rejected by the next.

So MetadataRawSearch.search sets params and json separately, the way 34 other resource builders already do: body stays a pure _search document, control stays in the query string. Keeping the builder on the resource is also what makes the routing assertable without httpx — see Testing.

A POST is never paginated (core.py tests method == 'GET'), so _single returns the single composite envelope rather than a generator — which is what we want here, since hits and aggregations arrive together.

Testing

Request shape — pure-unit tier (specs/04-testing.md invariant 7). What these pin is that the body arrives byte-for-byte and that the community is not in it. Neither is visible from the far side of a live call, which only shows that some scoped search ran. They read request_parameters, the projection of what is sent — PolyswarmRequest.json is reused for the parsed response and is None until the call runs. A mutation folding the community into the body kills three of them.

Lifecycle — live e2e, one cassette per transport. Recorded against a freshly booted stack running the matching server branch, verified to pass with TESTS_VCR=off against that stack and to replay offline for the unit jobs. Full suite: 410 passed; unasync mirror regenerates byte-identical.

Two things found while recording, both worth knowing:

  • The metadata document has no artifact.mimetype — the mimetype it carries is scan.mimetype. A terms aggregation on an unmapped field is not an error; it returns an empty bucket list, so the test failed with nothing to say why. The query now aggregates on extension, which is present and aggregatable.
  • A periodic server-side dispatch step is the only thing that turns a claimed metadata row into an Elasticsearch write. Without it no metadata-* index is ever created, and every metadata test — including the pre-existing test_metadata_search — polls an empty corpus until it gives up. Worth remembering when a local stack is brought up with commands disabled.

Requires — merge order is load-bearing

Merge in this order. Merging this PR first turns the e2e stage red in every repo, not just here.

  1. The entitlement service — declares the feature tag. Until it is on :latest, the entitlement check answers 400, which the server turns into a BadResponseException on every call.
  2. The server-side API — serves POST /v3/search/metadata/raw. Until it is on :latest, the route 404s.
  3. This PR.

Why the blast radius is wider than this repo: merging to develop promotes the SDK test image to :latest, and "every other repo's e2e pipeline resolves polyswarm-api:latest by default" (AGENTS.md). That image runs this whole suite with TESTS_VCR=off (docker/Dockerfile) against a stack whose other services are their :latest. So the two new live tests would POST a route that stack does not serve. This PR's own e2e is green because the identical branch name pairs it with the matching server image — a pairing unrelated pipelines do not get.

AGENTS.md already states the rule this follows: "New endpoints land there first; the SDK PR comes after."

The live tests fail rather than skip on a 404, deliberately. poll_equals treats NotFoundException as "the index has not caught up", which is right for a document and wrong for an endpoint — a search matching nothing is a 200 with total 0, so a 404 means the route is absent. Absorbed as lag it burned 90 polls and then failed as assert None == 1, naming nothing. A skip would be worse than either: the e2e job runs TESTS_VCR=off, so a renamed or withdrawn route would report green forever and take the :latest promotion with it. Failing is what makes the ordering above enforce itself instead of rotting into a permanently-skipped test. It costs no extra request — the first poll is the probe — so the cassettes still replay.

Only the live tier depends on any of this; the pure-unit tier and the recorded cassettes are self-contained.

Merging

Squash-merge this one, with a written message rather than the default. Two things otherwise reach the public history on develop: the head branch name carries an internal ticket ID, and GitHub's default squash message concatenates every commit body — several of which describe intermediate states this branch later reversed. The first commit's inline-dict _single form became a builder in 3d2bfbe, and the reader that "skips with the reason" became a hard failure in c351e76. Each body is accurate for the commit it belongs to; concatenated into one message they read as a description of the final state, which they are not.

The PR title plus a two-line summary is enough.

The server-side API gained POST /v3/search/metadata/raw, which runs a
caller-authored OpenSearch query body and overlays the tenant filter
itself. This is the client for it.

The body is passed through untouched and the community travels in the
query string, which is why the method uses the inline-dict _single form
rather than the generic builder: on a POST the builder puts every
parameter in the JSON body, and the server reads community/communities
out of that body when it resolves scope. A community folded into the
body would therefore choose the scope via a key the server's own
grammar then rejects.

A POST is never paginated, so _single returns the one composite result -
hits and aggregations together - rather than a generator.

Tests are in the respx tier rather than the e2e one: what they pin is
that the body arrives verbatim and the community is not in it, and
neither is visible from the far side of a live call. The live lifecycle
test follows separately.
A VCR lifecycle test per transport, each with its own cassette recorded
against a freshly booted stack running the matching server branch. Both
replay offline for the unit jobs and both pass with TESTS_VCR=off against
the live stack, which is the invariant that keeps the cassettes an
efficiency cache rather than the thing under test.

The query aggregates on `extension` rather than a mimetype field: the
metadata document has no artifact.mimetype at all (the mimetype it does
carry is scan.mimetype), and a terms aggregation on an unmapped field is
not an error - it returns an empty bucket list, so the assertion fails
with nothing to say why. The respx example moved to scan.mimetype for the
same reason, even though a mock would have accepted either.

Recording these needed the stack's ai-es-dispatch-pending step: it is the
only thing that turns a claimed es_metadata row into an Elasticsearch
write, so without it the metadata index is never created and every
metadata test polls an empty corpus until it gives up.
@claude

claude Bot commented Oct 16, 2026

Copy link
Copy Markdown

Review

The code is correct. The inline _single dict is the documented pattern for the /search/metadata/* methods, and keeping community in the query string is right. The sync mirror matches the async source, and the PR targets develop with no version bump. Two things need action:

1. Spec drift: specs/ is not updated. AGENTS.md, step 5 of "When adding a new resource", says to "Update specs/03-endpoints.md (and any other relevant spec) in the same PR." This PR changes no spec files. Missing:

  • specs/03-endpoints.md: add metadata_raw_search(body, community=None) as an inline (POST) entry next to metadata_mapping() (around L37). Note that community goes in the query string, the body is passed through unchanged, and the result is a single composite, not paginated.
  • specs/02-resources.md: add MetadataRawSearch # /search/metadata/raw to the resource tree (L31–33).
  • specs/04-testing.md: add test/metadata_raw_search_test.py to the test-file list (L23–36). If it stays a respx module, also add it to the list of ClientTestCase users in §65.

2. Tests are in the wrong tier for request-shape checks (test/metadata_raw_search_test.py). specs/04-testing.md invariant 7 puts "request-shape assertions (body vs query routing, None-omission…)" in the pure-unit tier, and §50 describes the standard split as "live-e2e VCR lifecycle + pure-unit". Respx is "only when the live e2e stack can't reasonably produce the scenario" (§45). The docstring argues that request shape can't be seen from a live call. That is the reason to use pure-unit, not respx. test/refang_test.py already shows how to capture the request dict at the _single boundary for both transports. Specifically:

  • test_the_body_reaches_the_server_verbatim, test_the_community_rides_the_query_string_not_the_body, test_an_explicit_community_overrides_the_clients and test_it_posts_to_the_raw_search_endpoint should be pure-unit assertions on the dict passed to _single.
  • test_hits_and_aggregations_are_both_reachable and test_a_complete_answer_carries_no_marker repeat what the new VCR lifecycle tests already pin with real data. Drop them.
  • test_a_partial_answer_keeps_its_marker is the only case the stack can't easily produce. It can stay on the respx harness, and its docstring should say why it can't run on e2e, as §315 requires.

poll_equals absorbs NotFoundException as "the index has not caught up".
That is right for a document and wrong for an endpoint: a search matching
nothing is a 200 with total 0, so a 404 from this route means the server
side is not deployed. Absorbed as lag it burned 90 polls and then failed as
`assert None == 1`, naming nothing - and because the e2e job runs with
TESTS_VCR=off against a live stack, that is what develop would do if this
landed before the server release, blocking the :latest promotion every
other repo's pipeline pulls.

The reader now skips with the reason instead. It costs no extra request -
the first poll is the probe - so the cassettes still replay. Drop it once
the route ships.

Also document two things the method's contract depends on and its docstring
left out: omitting `size` returns no documents, because the server defaults
it to 0 so an aggregation caller does not pay for hits they did not ask
for; and `partial` is a list of reasons, whose absence - not an empty
`hits` - is what says a count is whole.
@claude

claude Bot commented Oct 16, 2026

Copy link
Copy Markdown

Review against AGENTS.md / specs. Base develop and no version bump are both correct.

1. Spec drift: specs/ not updated. AGENTS.md "When adding a new resource" step 5 says to update specs/03-endpoints.md in the same PR, and "if a PR drifts from the spec, the spec is wrong until proven otherwise". This PR does neither of these:

  • specs/03-endpoints.md has no row for metadata_raw_search. It is an inline-dict _single POST, like metadata_mapping. The row should record the decisions the PR body says matter: community goes in the query string and never in the body, the body is passed through verbatim, the server defaults size to 0, and the partial marker is preserved.
  • specs/02-resources.md resource tree (~L31–33) is missing MetadataRawSearch # /search/metadata/raw.
  • The per-call community= override is a new kwarg pattern on the public surface. No other endpoint method takes one. It is worth a line in specs/05-downstream-contract.md, or at least in the 03 row.

2. E2e tests skip on any 404, which can hide SDK regressions (test/client_scan_test.py:86-88, test/async_client_test.py:306-307). A NotFoundException becomes pytest.skip. If the SDK path or RESOURCE_ENDPOINT regressed, these tests would skip instead of failing, both in the e2e CI job (TESTS_VCR=off) and whenever someone re-records a cassette. The SDK-owned path is exactly what a lifecycle test exists to catch. AGENTS.md: "tests must pass against the live e2e stack with VCR off… If a test only works against the recorded cassette, that's a bug in the test." Gate on the server release/feature flag explicitly, or drop the skip once the server side lands. Don't turn every 404 into a skip.

3. The docstring and the tests disagree on the shape of partial. aio/api.py:310 (and the sync mirror) documents partial as "a list of reasons". test/metadata_raw_search_test.py:75-77 pins it as a dict ({'terminated_early': True}). One of them is wrong. Fix whichever it is to match the server contract, because downstream callers will code against the docstring.

The docstring told callers that partial's absence means a count is whole.
It does not: the server suppresses the hit-total signal when the body
asked for no hits, because a total capped at track_total_hits is what
every large query returns and reporting it would mark all of them partial.

Exactness of a count is hits.total.relation - eq or gte - which is the
engine's own field and passes through untouched. partial speaks for the
aggregations and the search as a whole.
@claude

claude Bot commented Oct 16, 2026

Copy link
Copy Markdown

Review against AGENTS.md and specs/:

  1. Spec drift: endpoint not added to the specs. AGENTS.md "When adding a new resource" step 5 says to update specs/03-endpoints.md in the same PR. The ### Metadata table there has no metadata_raw_search(body, community=None) row. Add it as inline (POST) — parsed into MetadataRawSearch, and note that the body is passed through verbatim while community goes in the query string. The resource tree in specs/02-resources.md (around line 31) also needs MetadataRawSearch # /search/metadata/raw. Without the row, the main design decision (why this method skips the builder) is recorded only in the docstring and the PR body.

  2. The partial fixture has the wrong shape. test/metadata_raw_search_test.py test_a_partial_answer_keeps_its_marker mocks partial={'terminated_early': True}, a dict. The docstring and the PR body both say partial is a list of reasons. The test passes either way because it only round-trips the value, but it documents the wrong contract. Use a list, e.g. partial=['timed_out'].

  3. The skip-on-404 in the live tests will hide a regression after the route ships. _raw_or_skip (client_scan_test.py) and its inline copy in test_async_metadata_raw_search turn a 404 into pytest.skip. The e2e job on develop runs with TESTS_VCR=off. If the route later goes missing (a server regression, or the feature flag off in the e2e stack), these tests skip quietly. That breaks the invariant that "tests must pass against the live e2e stack with VCR off", and develop's e2e job would still pass and promote :latest. The comment says "Drop this once the route ships", but nothing tracks that. Either:

    • remove the skip and rely on the existing ## Requires ordering (the cassettes were already recorded against a stack that has the route), or
    • add an entry to specs/99-open-questions.md so the skip is removed when the server endpoint lands.
  4. Ticket ID in the branch name (minor). The head branch DN-8634-metadata-raw-search puts an internal ticket ID on the public repo. AGENTS.md "Commit + PR hygiene" bans these in commit messages, PR titles and PR descriptions, and the branch name exposes the ID the same way. Rename the branch if that's practical; otherwise just avoid it next time.

Gitflow is fine: the PR targets develop and doesn't bump the version, which is correct for a new additive method.

…specs

AGENTS.md step 5 asks for specs/03-endpoints.md in the same PR as a new
resource, and the class tree in specs/02-resources.md lists every
BaseJsonResource. Both omitted this one.
@claude

claude Bot commented Oct 16, 2026

Copy link
Copy Markdown

Review against AGENTS.md / specs. Base develop, no version bump, sync mirror matches, specs 02/03 updated. Two things need changing:

  1. The live tests skip on a 404, so the e2e job can't catch a regression (test/client_scan_test.py _raw_or_skip, test/async_client_test.py _hit_count). The e2e CI job runs TESTS_VCR=off. With this change, if the route goes missing or gets renamed, the test reports a skip instead of a failure, and the :latest image still gets promoted. AGENTS.md says "New endpoints land there first; the SDK PR comes after", and the PR's own Requires section says the server endpoint and flag must be deployed first. So the skip works around a merge-ordering problem the gitflow already rules out. "Drop this once the route ships" also has nothing to make it happen. Fix: hold this PR until the server change is on the e2e stack and remove the skip. If you do want a clearer message, keep the early exit on 404 but make it pytest.fail(...), not pytest.skip.

  2. The partial shape in the respx test doesn't match the documented contract. The docstring and PR body say partial is "a list of reasons". test_a_partial_answer_keeps_its_marker mocks it as a dict, {'terminated_early': True}. The test only checks that the key passes through, but it shows the wrong shape to anyone reading it. Use a list, e.g. ['timed_out'].

The e2e job runs TESTS_VCR=off against a live stack, so a skip let a
renamed or withdrawn route report green and take the :latest promotion
with it. AGENTS.md already orders the server endpoint first, so a 404 is
a real failure rather than a merge-ordering inconvenience to absorb.

The early exit stays: it still names the cause immediately instead of
spending 90 polls to fail as `assert None == 1`. Renamed to _raw_hit_count,
since it no longer skips.
The fixture mocked a dict, so the test demonstrated a shape the server
never sends and the docstring does not promise.
@claude

claude Bot commented Oct 16, 2026

Copy link
Copy Markdown

Checked against AGENTS.md and specs/02, 03, 04. The endpoint code is correct: the body goes through unchanged, community goes in the query string, the sync mirror matches, and both VCR lifecycle tests follow the convention. Two things need action.

1. Request-shape tests are in the wrong tier, because there is no builder (spec drift, specs/04 invariant 7).
specs/04-testing.md says request-shape assertions ("body vs query routing, None-omission") belong in the pure-unit tier, and that the canonical split for a new endpoint is "a live-e2e VCR lifecycle test + pure-unit builder tests". It also says respx is "reserved for scenarios the e2e stack can't produce". This PR's argument is that the generic builder puts everything in the POST body. That is true, but a hand-written classmethod builder doesn't have that problem, and resources.py already has many of them that set params and json separately (IOC.ioc_search, IOC.create_known_good, etc.). Suggest:

class MetadataRawSearch(core.BaseJsonResource):
    RESOURCE_ENDPOINT = '/search/metadata/raw'

    @classmethod
    def search(cls, api, body, community=None):
        return core.PolyswarmRequest(api=api, method='POST',
                                     url=f'{api.uri}{cls.RESOURCE_ENDPOINT}',
                                     params={'community': community or api.community},
                                     json=body, result_parser=cls)

Then aio/api.py becomes return await self._single(resources.MetadataRawSearch.search(self, body, community)), and the routing tests in test/metadata_raw_search_test.py (body verbatim, community not in body, explicit community override, URL) become pure-unit assertions on the returned PolyswarmRequest, with no respx. The partial / hits+aggregations passthrough checks only test BaseJsonResource keeping json intact. They can be pure-unit parse_response tests, or be dropped because the VCR lifecycle test already asserts both. Also update the specs/03-endpoints.md row from "inline (POST)" to the builder.

2. Gitflow / hygiene: the head branch DN-8634-metadata-raw-search contains an internal ticket ID.
AGENTS.md: "Don't reference ticket IDs or internal project codes … This repo is public; published artefacts shouldn't leak internal references." Branch names are public and stay in merge commits. The branch also doesn't follow the documented feature/* pattern. Please rename it (e.g. feature/metadata-raw-search) and re-open against develop, or at least make sure the merge commit message doesn't carry the ticket ID.

Not blocking: no version bump is correct here, since this is an additive method and the CLI pins published releases. Base develop ✓.

…-unit

AGENTS.md routes a new resource through a builder classmethod on the
resource, and specs/04 invariant 7 puts request-shape assertions in the
pure-unit tier. The inline request dict forced the opposite: body-vs-query
routing could only be observed through respx, which that invariant reserves
for what the e2e stack cannot produce.

MetadataRawSearch.search returns the PolyswarmRequest, so the routing reads
off request_parameters with no httpx and no transport parametrisation. A
mutation folding the community into the body kills three of the new tests.
@claude

claude Bot commented Oct 16, 2026

Copy link
Copy Markdown

The code is clean against AGENTS.md and specs/. The MetadataRawSearch.search builder routes community to the query string and the body verbatim, the sync mirror matches, specs/02 and specs/03 are updated, there are pure-unit plus VCR lifecycle tests per transport, the PR targets develop, and the version is not bumped. Two things to fix in the PR metadata:

  1. The PR description is stale against the final code. It still says the method uses "the inline-dict _single escape hatch" and that the tests are in the "respx tier", with "7 request-shape tests × 2 transports". The last commit (refactor(resources): give metadata_raw_search a builder…) replaced both: there is now a MetadataRawSearch.search builder, and test/metadata_raw_search_test.py is pure-unit (4 request-shape + 3 parse tests, run once). specs/03 already describes the builder correctly. Please rewrite the "Why the inline _single form" and "Request shape — respx tier" sections. If this is squash-merged, also drop the first commit's "inline-dict _single form" / "respx tier" reasoning from the squashed message.
  2. The head branch DN-8634-metadata-raw-search contains an internal ticket ID. AGENTS.md: "Don't reference ticket IDs or internal project codes… This repo is public." The branch name shows up in the PR and in the default merge-commit subject (Merge pull request #331 from …/DN-8634-…). Use a squash or rebase merge, or edit the merge subject, so the ID doesn't end up in develop history.

The docstring stated the exception as the rule: it said a capped total is
not reported as partial, when the server reports exactly that whenever the
body asked for hits, and omits it only when it asked for none. A caller
following the old text would read a complete aggregation as truncated and
re-run a narrowed query, paying for another metered search that can never
clear the marker.
@claude

claude Bot commented Oct 16, 2026

Copy link
Copy Markdown

Code, spec updates (02-resources.md, 03-endpoints.md), sync mirror and test tiers all match AGENTS.md / specs/04-testing.md. Base is develop and there is no version bump, which is correct for an additive method. Three things need action:

  1. Hygiene (AGENTS.md § Commit + PR hygiene). The description names api-key-management, which is not one of the public companion repos AGENTS.md lists. Refer to it by category, e.g. "the entitlement service". The branch name DN-8634-… carries a ticket ID, and it will land in the public history through the merge-commit subject ("Merge pull request feat: add metadata_raw_search for the raw metadata search endpoint #331 from …/DN-8634-…"). Squash-merge or rename the branch. The internal job name ai-es-dispatch-pending probably doesn't belong in a public PR body either.

  2. The metadata_raw_search docstring contradicts itself on partial (aio/api.py, mirrored in api.py). One paragraph says partial "speaks for the aggregations and the search as a whole". The next says a large hits-requesting query "normally has one — it is not a sign the aggregations are truncated". So a caller can't tell a capped-total reason from a truncated-aggregation reason by the key being present. The reasons look like free-form prose ('the search stopped at terminate_after' in the unit test), so they can't be told apart by matching on the text either. Either document a stable reason vocabulary the caller can branch on, or tighten the wording so partial reads as "inspect the reasons" rather than as a single completeness flag. That wording becomes part of the downstream contract, so settle it now.

  3. Nit, not caught by the formatter: test/client_scan_test.py has three blank lines before _raw_hit_count.

The two paragraphs contradicted each other: one said partial speaks for
the search as a whole, the next that a large query normally carries one.
Both are true of different reasons, which is the point - so the docstring
now names the closed set of codes and says to branch on them. The reasons
are prose no program can discriminate, which the server fixed by giving
each a code.

Also drops a stray blank line before _raw_hit_count.
@claude

claude Bot commented Oct 17, 2026

Copy link
Copy Markdown

Checked against AGENTS.md and specs 02, 03 and 04. The code is correct and follows the conventions. The builder is pure and keeps community in the query string, the same way the other resource builders do. The sync mirror matches its async source. Each transport has a live-e2e VCR test with its own cassette, plus pure-unit tests for the request shape and the parse. The cassettes have the auth header masked. The base branch is develop, there's no version bump, and the commit messages have no ticket refs or attribution. Two small things to fix:

  1. Spec drift: specs/04-testing.md. The test-file list there (around lines 21–36) names every pure-unit and builder test module, for example hunt_tracking_builder_test.py, ioc_search_test.py and known_good_test.py. The new test/metadata_raw_search_test.py is missing from it. AGENTS.md says to "Update the spec in the same PR as the code change", so please add an entry for it.
  2. Ticket ID in the branch name. DN-8634-metadata-raw-search will appear in the default merge-commit message ("Merge pull request feat: add metadata_raw_search for the raw metadata search endpoint #331 from polyswarm/DN-8634-…"). That puts an internal ticket ID into public git history, which AGENTS.md says to avoid. Either squash-merge with a clean message, or edit the merge message before merging.

The merge order in the PR description is important (entitlement service first, then the server, then this PR). Merging to develop promotes polyswarm-api:latest for every repo's e2e pipeline, so this has to wait until both upstream images are on :latest.

AGENTS.md asks for the spec in the same PR as the code, and the Files
list in specs/04-testing.md names every test module. specs/02 and 03
were updated for this endpoint; 04 was missed.
@claude

claude Bot commented Oct 19, 2026

Copy link
Copy Markdown

Reviewed against AGENTS.md and specs/02, 03, 04, and 05. The code is clean: MetadataRawSearch.search follows the builder convention, puts community in the query string and keeps the body as-is, and _single returns a single result because a POST is never paginated. The sync mirror matches. specs 02, 03 and 04 are updated. The PR targets develop with no version bump, which is correct for an additive method unless a CLI PR built from source needs a higher polyswarm_api>= floor. Tests cover both tiers (pure-unit request shape and parsing, plus live VCR tests for each transport).

One thing to fix before merge:

  • Ticket ID in the branch name / stale commit history. The head branch DN-8634-metadata-raw-search contains an internal ticket ID. A merge commit ("Merge branch 'DN-8634-…'" or "Merge pull request feat: add metadata_raw_search for the raw metadata search endpoint #331 from …/DN-8634-…") would put it into the public history, which AGENTS.md rules out. Several commit bodies are also out of date: the first commit says the tests are "in the respx tier" and use "the inline-dict _single form", and the third says the reader "skips… Drop it once the route ships". Later commits reversed all of these. Squash-merge with a clean message (the PR title plus a short summary) so neither the ticket ID nor the outdated claims reach develop.

@sbneto
sbneto requested a review from a team October 19, 2026 17:46
@sbneto
sbneto merged commit 17d2e8d into develop Oct 20, 2026
2 checks passed
@sbneto
sbneto deleted the DN-8634-metadata-raw-search branch October 20, 2026 15:56
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