Skip to content

Forward the guest curator election - #106

Merged
feruzm merged 3 commits into
mainfrom
feature/curation-elections
Sep 22, 2026
Merged

feruzm merged 3 commits into
mainfrom
feature/curation-elections

Conversation

@feruzm

@feruzm feruzm commented Sep 22, 2026 •

Copy link
Copy Markdown
Member

Closes #105. Pairs with ecency/esync-py#79, which ships first.

One new write, application-vote, carrying applicant, vote and an optional note. The account being voted on travels as applicant for the same reason a decision does: username carries the caller this gateway validated, and Build() refuses to copy a client's version of it, so a reviewer naming themselves would only ever vote on their own row.

application-window gains quorum and term_days; roster-set gains term_days.

What the fence actually checks

All three are optional and all three are validated when present, null included. CopyIfPresent forwards a null through the allowlist and upstream reads a null as absent, so a "quorum": null would quietly do nothing while this fence claimed it had checked a number in range. That is the hole roster rules had.

term_days accepts 0 on roster-set and refuses it on the window. They are different questions: on a seat, 0 is the one way to say "stop expiring", while a zero-day round would be a window nobody can be elected in.

trial leaves ApplicationRoles. A guest seat is trailed and bounded by its term, so the backend no longer offers an untrailed one.

Verification

  • 582 tests green. New cases cover the vote payload and every refusal string, rune-counted note caps, the present-null knob, the optional-knob passthrough and the zero term.
  • Three mutations run against the new guards: letting a present null through, dropping the vote-value check and raising the seat term floor to 1 each turns tests red.
  • application-vote added to the parity driver's route list.

Summary by CodeRabbit

  • New Features

    • Added a curation desk application-vote endpoint supporting endorse, object, and abstain decisions.
    • Added optional quorum and term-length settings to application windows and roster assignments.
    • Seats can now be made permanent with a zero-day term.
  • Validation

    • Added validation for applicant accounts, vote types, notes, quorum, and term lengths.
    • Notes are limited by character count, including Unicode characters.
  • Changes

    • The trial application role is no longer accepted.

One new write, application-vote, carrying the applicant, the vote and an
optional note. The account being voted on travels as `applicant` for the
same reason a decision does: `username` carries the caller this gateway
validated and Build() refuses a client's version of it, so a reviewer
naming themselves would only ever vote on their own row.

The window write gains `quorum` and `term_days`, and roster-set gains
`term_days`. All three are optional and all three are checked when
PRESENT, null included: CopyIfPresent forwards a null through the
allowlist, and upstream reads a null as absent, so a `"quorum": null`
would quietly do nothing while this fence claimed it had checked a number
in range. That is the hole roster `rules` had.

`term_days` accepts 0 on roster-set and refuses it on the window. They
are different questions: on a seat, 0 is the one way to say "stop
expiring", while a zero-day round would be a window nobody can be elected
in.

`trial` leaves ApplicationRoles. A guest seat is trailed and bounded by
its term, so the backend no longer offers an untrailed one.
An objection could only be lifted by endorsing, which made somebody who
merely stopped objecting add a vote toward the quorum they were doubting.
The backend gained a third value; the fence has to allow it or the button
would be refused at the edge.
@qodo-code-review

Copy link
Copy Markdown

ⓘ Qodo reviews are paused because your trial has ended. Ask your workspace admin to add credits to resume reviews. Manage billing

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Forward guest curator election voting and term controls

✨ Enhancement 🧪 Tests 🕐 20-40 Minutes

Grey Divider

AI Description

• Adds endorse, object, and abstain voting for guest curator applications.
• Forwards validated quorum and term settings for election windows and roster seats.
• Removes untrailed trial grants and expands payload and parity coverage.
Diagram

sequenceDiagram
    actor Reviewer
    participant Routes as HTTP Routes
    participant Handler as Private API
    participant Fence as Payload Fence
    participant Desk as Upstream Desk
    Reviewer->>Routes: Submit election write
    Routes->>Handler: Dispatch route
    Handler->>Fence: Validate and whitelist
    alt Valid payload
        Fence->>Desk: Forward caller and fields
        Desk-->>Reviewer: Election result
    else Invalid payload
        Fence-->>Reviewer: Refusal message
    end
Loading
High-Level Assessment

The PR appropriately extends the existing generic desk-write routing, allowlisting, and validation path instead of introducing a dedicated voting stack. Separate request models or handlers would add duplication without improving the small, route-specific contract; the focused validator and comprehensive boundary tests are the best fit.

Files changed (5) +194 / -11

Enhancement (2) +78 / -5
PrivateApi.CurationDesk.csImplement and validate guest curator election writes +77/-5

Implement and validate guest curator election writes

• Adds the application-vote write with endorse, object, and abstain ballots, applicant validation, and optional rune-limited notes. Extends election windows with quorum and term controls, roster seats with tri-state term handling, removes trial grants, and rejects present invalid or null numeric settings.

dotnet/EcencyApi/Handlers/PrivateApi.CurationDesk.cs

Routes.csExpose the private application-vote endpoint +1/-0

Expose the private application-vote endpoint

• Maps POST requests for '/private-api/curation-desk/application-vote' to the new curation desk handler.

dotnet/EcencyApi/Handlers/Routes.cs

Tests (3) +116 / -6
CurationDeskPayloadTests.csCover election votes, quorum, and seat-term payload contracts +110/-4

Cover election votes, quorum, and seat-term payload contracts

• Adds payload tests for application voting, including abstention, applicant identity, vote values, and rune-counted notes. Covers optional quorum and term forwarding, present-null rejection, range boundaries, permanent seats, and removal of the trial role.

dotnet/EcencyApi.Tests/CurationDeskPayloadTests.cs

CurationDeskTestSupport.csAdd application voting to shared route test cases +3/-1

Add application voting to shared route test cases

• Updates accepted application examples to use the curator role and adds a valid application-vote request to the common private API route cases.

dotnet/EcencyApi.Tests/CurationDeskTestSupport.cs

driver.pyInclude application voting in parity route coverage +3/-1

Include application voting in parity route coverage

• Adds application-vote to the parity driver's guest curator route matrix so minimum, populated, and invalid-code payload behavior remains aligned.

dotnet/parity/driver.py

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Sep 22, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (1) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (1) 📜 Skill insights (0)

Grey Divider


Action required

1. Default promotions now fail 🔗 Cross-repo conflict ≡ Correctness
Description
ApplicationRoles drops trial, and ApplicationDecide rejects any supplied role outside that
set. vision-web initializes every application to trial and forwards the selected role on Promote,
so its default promotion now receives a 400 response.
Code

dotnet/EcencyApi/Handlers/PrivateApi.CurationDesk.cs[R824-825]

  public static readonly IReadOnlySet<string> ApplicationRoles =
-        new HashSet<string> { "trial", "curator", "mod" };
+        new HashSet<string> { "curator", "mod" };
Evidence
The PR removes trial from the API allowlist, and the existing decision validator rejects roles
outside that list. The pinned vision-web client still declares trial as an available seat,
initializes each row to it, and sends that selected role when Promote invokes the decision request.

External repo: ecency/vision-web, apps/web/src/features/curation-desk/curation-applications-panel.tsx \\\[47-48\\\]
External repo: ecency/vision-web, apps/web/src/features/curation-desk/curation-applications-panel.tsx \\\[77-88\\\]
External repo: ecency/vision-web, apps/web/src/features/curation-desk/curation-applications-panel.tsx \\\[148-153\\\]
External repo: ecency/vision-web, packages/sdk/src/modules/curation/requests.ts \\\[463-478\\\]
dotnet/EcencyApi/Handlers/PrivateApi.CurationDesk.cs[821-825]
dotnet/EcencyApi/Handlers/PrivateApi.CurationDesk.cs[1241-1255]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The API no longer accepts `trial` for application decisions, while vision-web still uses `trial` as the default role for every promotion. Preserve compatibility until the web client has switched its seat options and default to a supported role.
## Fix Focus Areas
- dotnet/EcencyApi/Handlers/PrivateApi.CurationDesk.cs[824-825]
## Recommended Fix
Keep `trial` in `ApplicationRoles` for the compatibility window, then update vision-web's application seat list and default to `curator`, deploy that client change first, and remove `trial` from the API afterward.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Vote handler breaks route convention 📘 Rule violation ⌂ Architecture
Description
CurationDeskApplicationVote is declared public static Task and forwards the task directly
instead of using the required public static async Task handler form. The method is newly wired to
/private-api/curation-desk/application-vote, so that route enters the handler table with a
signature that differs from the mandated contract.
Code

dotnet/EcencyApi/Handlers/PrivateApi.CurationDesk.cs[R481-482]

+    public static Task CurationDeskApplicationVote(HttpContext ctx) =>
+        ServeDeskWrite(ctx, CurationDeskWrites.ApplicationVote);
Evidence
Compliance rule 2667961 requires every mapped handler to be declared public static async Task with
exactly one HttpContext ctx parameter. The new method has the correct class, visibility,
parameter, and one-to-one route mapping, but omits async and is directly referenced by the new
route.

Rule 2667961: Enforce uniform HTTP handler method signature and placement
dotnet/EcencyApi/Handlers/PrivateApi.CurationDesk.cs[481-482]
dotnet/EcencyApi/Handlers/Routes.cs[202-202]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new application-vote route handler returns `Task` directly instead of using the required `public static async Task` handler signature.
## Fix Focus Areas
- dotnet/EcencyApi/Handlers/PrivateApi.CurationDesk.cs[481-482]
## Recommended Fix
Declare `CurationDeskApplicationVote` as `public static async Task CurationDeskApplicationVote(HttpContext ctx)` and await `ServeDeskWrite(ctx, CurationDeskWrites.ApplicationVote)` inside the method.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Whole-number settings are rejected 🐞 Bug ≡ Correctness
Description
RequireWholeNumber uses TryGetValue, which accepts only integer-form JSON tokens rather than
every numeric value that is mathematically whole. When a client sends an in-range setting as 4.0
or 4e0, validation returns a 400 for quorum and both term fields even though the repository's
existing numeric contract treats integral JSON exponent values as whole numbers.
Code

dotnet/EcencyApi/Handlers/PrivateApi.CurationDesk.cs[R1415-1416]

+        if (node is not JsonValue value || value.GetValueKind() is not JsonValueKind.Number
+            || !value.TryGetValue<int>(out var number) || number < min || number > max)
Evidence
The new helper validates all three added settings through TryGetValue. Existing curation-desk
tests explicitly establish that JSON exponent notation represents the same numeric value as its
integer spelling and that an integral exponent value is retained, demonstrating that whole-number
handling elsewhere is value-based rather than spelling-based.

dotnet/EcencyApi/Handlers/PrivateApi.CurationDesk.cs[1291-1296]
dotnet/EcencyApi/Handlers/PrivateApi.CurationDesk.cs[1315-1318]
dotnet/EcencyApi/Handlers/PrivateApi.CurationDesk.cs[1407-1418]
dotnet/EcencyApi.Tests/CurationDeskPayloadTests.cs[403-410]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`RequireWholeNumber` rejects integral JSON numbers written with a decimal or exponent, such as `4.0` and `4e0`, because `TryGetValue<int>` is representation-sensitive.
## Fix Focus Areas
- dotnet/EcencyApi/Handlers/PrivateApi.CurationDesk.cs[1412-1420]
- dotnet/EcencyApi.Tests/CurationDeskPayloadTests.cs[803-849]
## Recommended Fix
Read the JSON number into a representation that supports decimal and exponent notation, verify that its numeric value is integral and within the requested range, and normalize the accepted field to an integer before `Build` copies it upstream. Add application-window and roster-set tests covering in-range integral decimal and exponent values while continuing to reject fractional values.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can route each action level your way: inline, summary, both, or drop

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread dotnet/EcencyApi/Handlers/PrivateApi.CurationDesk.cs
Comment thread dotnet/EcencyApi/Handlers/PrivateApi.CurationDesk.cs
Comment thread dotnet/EcencyApi/Handlers/PrivateApi.CurationDesk.cs
@coderabbitai

coderabbitai Bot commented Sep 22, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Warning

Review limit reached

Next included review available in 43 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: bcd27e56-102f-42d5-8259-b812c0f59818

📥 Commits

Reviewing files that changed from the base of the PR and between f7b9fed and 7485fcf.

📒 Files selected for processing (2)
  • dotnet/EcencyApi.Tests/CurationDeskPayloadTests.cs
  • dotnet/EcencyApi/Handlers/PrivateApi.CurationDesk.cs

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 197f65ee-77d0-40d4-a520-4d37b171c619

📥 Commits

Reviewing files that changed from the base of the PR and between fedd2a2 and f7b9fed.

📒 Files selected for processing (5)
  • dotnet/EcencyApi.Tests/CurationDeskPayloadTests.cs
  • dotnet/EcencyApi.Tests/CurationDeskTestSupport.cs
  • dotnet/EcencyApi/Handlers/PrivateApi.CurationDesk.cs
  • dotnet/EcencyApi/Handlers/Routes.cs
  • dotnet/parity/driver.py

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The gateway adds the application-vote write route. It validates vote payloads, rune-based note lengths, election knobs, and seat terms. It also removes trial from application roles and updates tests and parity coverage.

Changes

Curation desk write expansion

Layer / File(s) Summary
Write contracts and validation
dotnet/EcencyApi/Handlers/PrivateApi.CurationDesk.cs
Adds application vote fields and vote values. Adds quorum, term_days, range checks, whole-number checks, and rune-based note validation. Removes trial from application roles.
Application vote routing
dotnet/EcencyApi/Handlers/PrivateApi.CurationDesk.cs, dotnet/EcencyApi/Handlers/Routes.cs
Adds the CurationDeskApplicationVote handler and registers the POST route.
Payload tests and parity coverage
dotnet/EcencyApi.Tests/CurationDeskPayloadTests.cs, dotnet/EcencyApi.Tests/CurationDeskTestSupport.cs, dotnet/parity/driver.py
Tests forwarding and rejection behavior for votes, notes, election knobs, and seat terms. Updates signed-write data and parity route catalogs.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Routes
  participant PrivateApi
  participant ServeDeskWrite
  Client->>Routes: POST application-vote
  Routes->>PrivateApi: CurationDeskApplicationVote
  PrivateApi->>ServeDeskWrite: Validate and forward ApplicationVote
  ServeDeskWrite-->>Client: Write result
Loading

Merge Risk: ⚪ Minimal · up to f7b9f

The gateway adds the election writes and validation coverage without a remaining merge-blocking issue.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 20 functions across 5 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: forwarding guest curator election support through the API.
Linked Issues check ✅ Passed Issue #105 coding requirements are implemented. The PR adds application-vote with applicant, vote, and optional note, while the validated caller remains separate. It adds optional quorum and…
Out of Scope Changes check ✅ Passed The changed handlers, route registration, parity-driver entries, and tests support the guest curator election forwarding required by issue #105. The parity entries for related application routes and t…
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

A rabbit checks each vote with care
Applicant fields travel there
Whole-number knobs stay in range
Seat terms mark the lasting change
The curation desk now hops ahead

Comment @coderabbitai help to get the list of available commands.

A term only expires a seat that may carry one, so `term_days` above 0 on a
mod or an admin is a value the backend would only reject. This gateway's job
is to refuse those here rather than spend an authenticated round trip
finding out. 0 stays legal on any role: it says no term, which is what a
permanent role already is.

Also restores the comment label the vote handler took from the window one.
@feruzm
feruzm merged commit d7ada7e into main Sep 22, 2026
4 checks passed
@feruzm
feruzm deleted the feature/curation-elections branch September 22, 2026 13:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Forward the guest curator election

1 participant