Skip to content

Direct annotation allocation enforcement

Boundary

Implement item 1 of STATUS.md, continuing the MVP merged in #2991. Do not enable any environment flag, introduce reviewer UI, or implement rebalancing. An allocation percentage remains a share of review slots, not a percentage of studies.

Implementation and acceptance map

Step Change Acceptance evidence
1 Extract the existing fail-closed bucket computation into a shared core policy Existing proportional next-study tests remain green; direct eligibility agrees with the same bucket plan
2 Guard GET .../studies/{studyId}/review before usage writes and reservation/presence setup Out-of-share or unconfigured reviewers receive 404 with no side effects; allocated reviewers retain access
3 Guard PUT .../studies/{studyId}/session/{sessionId} before mapping and calling the submission orchestrator Out-of-share new annotation submissions return 404 without calling the writer; approved submissions still save
4 Bind the flagged ordinary submission's body stage/session IDs to its route A caller cannot check eligibility against one stage or saved session and write another; mismatches return 400
5 Preserve saved work and existing feature boundaries Saved ordinary sessions must match the reviewer and stage, plus the submitted session ID on writes; another stage/reviewer or a reconciliation session cannot grant an exemption
5a Recheck eligibility against each freshly loaded submission attempt Concurrent deletion of an exempt saved session cannot recreate out-of-share work on a retry; denial precedes session mutation and guarded save
6 Validate and document the slice Focused core and API tests, broader relevant .NET tests/build, documentation validation, and a reviewable PR with results

Compatibility

  • Allocation is checked only when the runtime flag is on; disabled/missing stage allocation retains existing eligibility behaviour. Flagged ordinary submissions must still have matching route/body stage and session IDs, even on stages without allocation.
  • Existing saved ordinary sessions (incomplete or completed) remain readable and editable despite stale allocation; this preserves resumption and correction, not creation of new sessions.
  • A slot reservation alone is not evidence of a saved session and cannot bypass direct endpoint eligibility. Random assignment's existing-reservation behaviour is unchanged.
  • Invalid reviewer membership or configuration fails closed for new work, without a reservation, usage write, submission, or server error. Existing authorization and capacity checks still apply.
  • Reconciliation reads and reconciliation submissions keep their existing behaviour. Submission mode is taken from the body used by the existing save path, not the unused query parameter.
  • No changes to screening, session deletion, SignalR protocol, or transaction/statistics persistence. The submission orchestrator repeats eligibility before mutating each loaded study, including retries. Membership lifecycle policy and cross-request configuration consistency remain later roadmap work.

Validation cases

  • Two reviewers with a one-review-per-study 50/50 split: direct read and new submission agree with each reviewer's bucket membership.
  • Flag off, stage allocation disabled, or no allocation: legacy behaviour.
  • Reviewer omitted; another configured member removed; invalid reviews-per-study: deny new work.
  • Saved own same-stage session: preserve access, including completing/correcting that exact session.
  • Other reviewer's session, other-stage session, reconciliation session, or forged body IDs: no saved-session exemption.
  • Reconciliation GET/PUT remain unfiltered; ?reconciliation=true cannot bypass an ordinary body.
  • Test active-reviewer tracking both on and off so it cannot accidentally gate allocation checks.
  • Delete or replace the exempt saved session between optimistic-concurrency attempts: return 404 without a second mutation/save. Retained exact sessions, allocated work, reconciliation and flag-off submissions retain their existing retry behaviour.

Follow-on order

Reviewer assignment UI and remaining-share count, then administrator allocation progress, membership lifecycle, third-party stage authorization, editor/save coordination, measured performance work, history-aware rebalancing, and only then optional onboarding/co-allocation work. The feature is not fully usable or ready for rollout merely because this first slice is implemented.

Implementation evidence (2026-09-05)

Steps 1–5 are implemented; step 6 has completed local validation and awaits PR review/merge. There are 51 new controller cases, ten submission-orchestrator cases and five new core policy cases, using the existing xUnit, Moq and mapper collection patterns. No new packages or database migrations are required.

  • Full SyRF.ProjectManagement.Core.Tests: 2,116 passed, one existing skipped test.
  • Full SyRF.API.Endpoint.Tests: 1,227 passed, none skipped (including the saved-session retry fix).
  • API and Project Management endpoint projects build successfully with .NET SDK 10.0.100.
  • Documentation validation completed with no errors and 62 warnings; git diff --check passed.
  • The broader suites include existing next-study, capacity, reconciliation, submission-retry, statistics-owner and allocation-configuration regression tests.

Reproduce from the worktree root:

dotnet test src/libs/project-management/SyRF.ProjectManagement.Core.Tests/SyRF.ProjectManagement.Core.Tests.csproj -m:2
dotnet test src/services/api/SyRF.API.Endpoint.Tests/SyRF.API.Endpoint.Tests.csproj -m:2
dotnet build src/services/project-management/SyRF.ProjectManagement.Endpoint/SyRF.ProjectManagement.Endpoint.csproj -m:2
./docs/scripts/validate-docs.sh --verbose
git diff --check

This is local code/test evidence, not preview or production evidence. No flags, environment configuration, or live data were changed. No browser UI is added in this slice.