Mill votes: ranked, paged read and not-yet-voted-by-this-network filter #41

Merged
heimdall merged 1 commit from heimdall/mill-ranked-read into main 2026-11-10 21:06:56 +00:00
Member

Adds GET /public/concepts/{collection}/ranked beside /tallies (unchanged) for collections of hundreds of ships.

  • Query (closed shape; anything else is 400 request_invalid): sort=net|total|newest (default net), limit=1..100 (default 50, canonical decimal only), cursor, unvoted=network.
  • Response: { items: [{ key, title, up, down, members: {up, down} }], next_cursor }. Order is largest first, ties by key ascending, so it is total. newest is registration time.
  • Cursor: opaque base64url of version, sort, filter, last value and key. Strict canonical decoding, length-capped, every field re-validated, bound to the sort and filter it was issued for. Keyset, so a vote landing between pages can move rows but never repeats the cursor's own row.
  • unvoted=network: leaves out concepts whose stored network tag equals the caller's tag (derived from the connection address with the existing NetworkPrefix.Tag; never accepted from the request, never returned, absent from the cursor). With no client address it filters nothing. The stored tag is the last caster's, as for purge. Answer is private, no-store; the plain read is public, max-age=60. Same CORS policy and Vary: Origin as the map.
  • Tests: ConceptRankedExportTests (78 cases: orders, paging walk, cap, closed query, 28 hostile cursors, hostile collections, no tag or voter identity anywhere, cursor binding, cache and CORS, read-only).
  • Known and accepted: unvoted=network tells a caller whether anyone on their own network (e.g. behind one NAT) has voted a concept, and the stored tag is the last caster's, so a voter moving networks reappears in the list. Each request loads the collection's subjects and counts (fine for hundreds; the unvoted answer is uncached and unthrottled, so tens of thousands would want a rate limit or a projection).
  • Not deployed; no change to the deploy pin.

Note: on a .NET 10 SDK the existing Results.Json tally tests fail on PipeWriter.UnflushedBytes under TestServer (baseline fails the same 8); the new route writes Results.Text(JsonSerializer...) so it is testable there. CI on .NET 8 is unaffected.

🤖 Generated with Claude Code

Adds `GET /public/concepts/{collection}/ranked` beside `/tallies` (unchanged) for collections of hundreds of ships. - Query (closed shape; anything else is `400 request_invalid`): `sort=net|total|newest` (default net), `limit=1..100` (default 50, canonical decimal only), `cursor`, `unvoted=network`. - Response: `{ items: [{ key, title, up, down, members: {up, down} }], next_cursor }`. Order is largest first, ties by key ascending, so it is total. `newest` is registration time. - Cursor: opaque base64url of version, sort, filter, last value and key. Strict canonical decoding, length-capped, every field re-validated, bound to the sort and filter it was issued for. Keyset, so a vote landing between pages can move rows but never repeats the cursor's own row. - `unvoted=network`: leaves out concepts whose stored network tag equals the caller's tag (derived from the connection address with the existing `NetworkPrefix.Tag`; never accepted from the request, never returned, absent from the cursor). With no client address it filters nothing. The stored tag is the last caster's, as for purge. Answer is `private, no-store`; the plain read is `public, max-age=60`. Same CORS policy and `Vary: Origin` as the map. - Tests: `ConceptRankedExportTests` (78 cases: orders, paging walk, cap, closed query, 28 hostile cursors, hostile collections, no tag or voter identity anywhere, cursor binding, cache and CORS, read-only). - Known and accepted: `unvoted=network` tells a caller whether anyone on their own network (e.g. behind one NAT) has voted a concept, and the stored tag is the last caster's, so a voter moving networks reappears in the list. Each request loads the collection's subjects and counts (fine for hundreds; the unvoted answer is uncached and unthrottled, so tens of thousands would want a rate limit or a projection). - Not deployed; no change to the deploy pin. Note: on a .NET 10 SDK the existing `Results.Json` tally tests fail on `PipeWriter.UnflushedBytes` under TestServer (baseline fails the same 8); the new route writes `Results.Text(JsonSerializer...)` so it is testable there. CI on .NET 8 is unaffected. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
GameCult/Bifrost!41
No description provided.