Skip to content

Review eligibility completion audit (S5, first pass)

Dated snapshot (25 September 2026)

This audit was written against main at 118424964 on 25 September 2026 and has not been re-baselined. Since then S4-A (#3732) has merged, so the rows that treat it as open or in flight (D7.2, MG.4, MG.6, WF.2, WF.4 and the "Concern to relay to S4-A" note) describe that date, not today. The eligibility programme is paused; the current plan for its remaining slices (X-ELIG) is in the integrated review plan.

This audit maps every decided requirement in the review eligibility policy and its truth table to the PRs that implement it and the tests that prove it.

Scope and method

  • Baseline. main at 118424964: #3736 merged, #3732 (S4-A) open.
  • Read-only. Nothing was run for this audit. A test counts as evidence only if it was found by name and its body or assertions were read. The policy doc's own claims were not trusted without that.
  • Decisions as written. The audit records what the doc decides. Where it finds a gap it doesn't propose new policy; the questions that need a decision are listed separately.
  • What is not re-audited. Items marked "proposal" in the doc are audited only where the lead accepted them as MVP criteria. Those are points 1, 2, 3 and 5 of "Proposed independent revocation and SignalR enforcement".

Classes

Class Meaning
Done Implemented, and a test on main proves it.
Done, thin Implemented, but the tests don't adequately prove it.
Partial Some of the requirement is missing.
Not done Not implemented.
S4-A / S4-B / S4-C Owned by the in-flight S4 slices: #3732 (grouped configuration, writer floor), migration tooling, and the settings API and editor.
Deferred (#n) Explicitly deferred by an open issue. No open issue defers any of the gaps below.

Path prefixes

Prefix Path
Core src/libs/project-management/SyRF.ProjectManagement.Core
CoreT …Core.Tests
MongoT …Mongo.Data.Tests
ApiT src/services/api/SyRF.API.Endpoint.Tests
PmT src/services/project-management/SyRF.ProjectManagement.Endpoint.Tests
Web src/services/web/src/app

Headline findings

  1. The server contract is largely built and tested. The reviewer browser does not consume it.
  2. The web app never reads NextStudyResponseDto.eligibility.
  3. The review page still applies #3551's universal screening-complete veto (Web/stage/stage-review/stage-review.component.ts screeningComplete, via screeningIsComplete). That veto ignores AdditionalScreeningPolicy.
  4. As a result, even with the flag on, Combined Allow (including existing unset stages under D1) is refused in the browser while the API accepts it.
  5. The ReconciliationNotReady warning, the reason copy, stale-study refetch and per-activity disabling are likewise absent.
  6. Only two causes emit the claim-revoked event: D8 decision 2 and Apply anyway.
  7. Screening completion releases claims on the server but sends no event.
  8. Permission or membership loss refuses admission but keeps the claim.
  9. Project-level threshold and default-target changes plan and revoke nothing.
  10. No browser (e2e) or staging acceptance exists for reviewEligibilityPolicy. Most of the concurrency matrix is also untested.
  11. Flag off is behaviourally identical. It is not byte-identical, however: there are unsanctioned wire additions (null/0 fields), inert new infrastructure (an index-init collection and a per-pod queue), a changed error detail, and one unflagged persisted-data change (FO.4, a data-preserving fix).
  12. The allocation interaction is correct. Flag off, #2991/#3603 behaviour is unchanged, and the D8(2) slot rule is tested at Core and Mongo level. The kept-claim continuation lacks endpoint-level tests.

1. Decisions D1–D8 and the truth table

ID Requirement (doc line) PR(s) Test evidence Class Gap / note
D1.1 Screening-only is always strict (L123, L1079) #3579, #3646 CoreT ReviewEligibilityPolicyTests::ScreeningOnlyIsImplicitlyStrictAfterSufficiency; StageReviewConfigurationTests::NewSinglePurposeInputCannotSupplyCombinedSettings Done
D1.2 An existing combined stage with no policy resolves to Allow (L124) #3579 StageReviewConfigurationTests::MissingExistingPolicyAllowsButNewCombinedStops, ::LegacyCombinedRoundTripDoesNotInventExplicitPolicy; ReviewEligibilityPolicyTests::ExistingCombinedStageWithMissingPolicyResolvesToAllow Done Server only; the browser refuses it (UI.1).
D1.3 Explicit combined choices are preserved (L124) #3579 StageReviewConfigurationTests::ExistingExplicitCombinedPolicyIsPreserved Done
D1.4 Annotation-only has no screening policy (L125) #3579, #3646 ReviewEligibilityPolicyTests::AnnotationOnlyHasNoScreeningPolicyAndOffersNoScreeningAction Done
D2.1 New combined stages default to Stop and persist it (L1080) #3579 ApiT ProjectControllerTests::AddStageUsesFlaggedConfigurationDefaults; StageReviewConfigurationTests::NewCombinedStopSurvivesBsonRoundTrip; Web create-stage.component.spec.ts Done
D2.2 New single-purpose stages accept no Allow/Stop input (L1080) #3579 ProjectControllerTests::AddStageRejectsInapplicablePolicyBeforeMutation Done
P.1 Strict refuses a new decision after screening is complete; Allow permits it (L127-129) #3579, #3691 ActivityReservationAdmissionTests::ScreeningSaveUsesCombinedPolicyAndRetainsOwnCorrection; ReviewEligibilityPolicyTests::CombinedStopRefuses…, ::CombinedAllowPermits… Done Server only.
P.2 A sufficient study never re-enters the new-screening pool (L128) #3646, #3659 ReviewEligibilityPolicyTests::ACompletedStudyNeverReturnsToTheNewScreeningPoolEvenUnderAllow; MongoT ReviewEligibilityPoolPredicateTests::PoolPredicatesMatchTheTruthTableRow Done
P.3 Claim set per configuration (L131-134) #3579, #3719 ActivityReservationPolicyTests::ClaimsFollowActivityAndStrictnessNotSelection Done
P.4 / D3b No person-level prerequisite (L137-141) #3646 ReviewEligibilityPolicyTests::AnnotationHasNoScreeningPrerequisite Done
P.5 Correcting one's own decision is separate from strictness (L143-147) #3646, #3579 ReviewEligibilityPolicyTests::CorrectionIsSeparateFromStrictnessAndScreeningCapacity Done
D3a.1 No closure concept; a reviewer's no-work state is not stage completion (L1027-1030) n/a absence confirmed by grep Done A negative requirement.
D3a.2 Fix the fixed-two StudyStats/AnnotationTallyStateProfile discrepancy before any code claims configured completion (L1048-1058) none none Not done Latent; no issue tracks it.
D4.1 Reconcile grant plus route/body identity (L99-103) #3565 ApiT ReviewControllerReconciliationAuthorizationTests (3 facts) Done Unflagged.
D4.2 Row ownership on saves (L1083) #3671, #3691 CoreT ExtractionInfoSessionOwnershipTests (~25 facts) Done Unflagged.
D4.3 Refuse the newly unavailable activity after a disable or mode change; keep work (L1083) #3579, #3691 ApiT ReviewWriteRefusalEndpointTests::FlagOnRefusalIsTypedAndLeavesTheStudyAndProjectUnchanged; StageReviewSettingsControllerTests::StageDisableRefusesFreshReviewAndLeavesDraftUncommitted Done
D4.4 Selection mode alone is not an action prohibition #3646 ReviewEligibilityPolicyTests::SelectionModeIsNotAnActionProhibition Done
D5.1 Exclusion removes a study only from the new-annotation pool #3646, #3659 ReviewEligibilityPolicyTests::SufficientExclusionOnlyRemovesTheStudyFromTheNewAnnotationPool Done
D5.2 Excluded-work visibility and grouping settings are preserved (L1084) #3579 indirect only Done, thin No test round-trips ExcludedSessionStatsGrouping/HideExcludedStudiesFromReviewers through the guarded PUT.
D6.1 Screening capacity uses the project rule, not the annotation target #3579 ActivityReservationPolicyTests (2); MongoT ActivityReviewAdmissionTests::ConcurrentCrossStageClaimsCannotTakeTheSameRemainingScreeningPlace Done
D6.2 A conflict on the first save writes nothing #3579 StageReviewSettingsControllerTests::ConflictIsReadOnlyAndForceReleasesOnlyExcessAnnotationClaims Done
D6.3 Apply anyway is for an authorized admin and overrides only the reservation conflict #3579 ApiT ::ForceCannotBypassCurrentDesignPermission, ::ForceDoesNotBypassConfigurationValidation, ::UnknownHistoryRequiresConversionConfirmationEvenWhenForceIsSet; CoreT ReviewSettingsConsentPolicyTests Done
D6.4 Recheck, then commit settings and revocations atomically against competing writes (L1085, L1449-1464) #3579 ::ClaimPinnedBeforeStageDisableCannotCommitAgainstTheOldSettings, ::ProjectSaveFailureRollsBackClaimReleases Partial No race of a confirmed Apply anyway against a concurrent claim or submission.
D6.5 The earliest valid reservations survive, with a stable tie-break #3579 ActivityReservationPolicyTests::ReducingScreeningCapacityRetainsEarliestEligibleClaimsWithoutTouchingAnnotation Done, thin The equal-timestamp Page.Id tie-break is untested.
D6.6 When completed work meets the target, remaining temporary claims are revoked (L1085, L815-830) #3579 screening case only Partial The annotation case is untested. It is skipped when the target isn't enforced, and "saved" counts incomplete sessions. Decision needed (Q1).
D6.7 Saved work and independently eligible activity are preserved #3579 ::ConflictIsReadOnly…, ::ConfirmedConversionRetainsSavedReviewsAndTheirPositiveHistoricalEvidence Done
D6.8 Activity-specific SignalR plus API/reconnect recovery #3736 ApiT ActivityClaimRevokedConsumerTests, ActivityClaimRevocationDispatchServiceTests; Web study-presence.init.spec.ts Partial Only two causes emit the event (RV.8), and there is no per-activity disabling (UI.1).
D7.1 Pure legacy mapping (four mismatches, schema-0, missing policy becomes Allow) #3579 StageReviewConfigurationTests::AllAuditedMismatchKindsRetainModeAndDeriveSelection, ::SchemaZeroBothFalseRetainsProductionScreeningMeaning Done Read-time only.
D7.2 Migration execution #3732 (open) – S4-A/S4-B
D8.1 Own annotation can be removed on a disabled or mode-changed stage #3646, #3691 ReviewEligibilityPolicyTests::RemovalNeedsOnlyTheReviewGrantAndOwnershipOnADisabledOrChangedStage; ApiT ::StageDisableStillAllowsOwnSessionRemovalOnTheCurrentSettingsRevision Done
D8.2a A leftover claim grants no exemption for new work #3646, #3719 ReviewEligibilityPolicyTests::ALeftoverClaimOutsideTheAllocation…; ActivityReservationClaimReleaseTests::SlotLeft_WithoutAClaimTheStudyIsStillNotNewWork Done
D8.2b Release only when no slot is left; a kept claim continues #3719, #3736 MongoT ActivityReviewAdmissionTests::ALeftoverClaimWithNoSlotLeftIsReleased…; ActivityReservationClaimReleaseTests (5 facts) Done Endpoint-level continuation is untested (AL.2).
D8.2c Under reallocation, claims are protected work; unaccommodated ones are released with a warning none – Deferred (#3745) Waits for allocation Phase 3 (lead, 25 September 2026).
D8.3 A disabled stage cannot be opened (UI guard plus server) #3646, #3659 Web stage-disabled-guard.service.spec.ts; ApiT NotificationHubStudyPresenceTests::JoinStudyReview_ShouldDenyDisabledStageBeforePresenceMutation Partial REST GetStudyForReview still returns 200 with the study, and the reconciliation GET has no refusal (RO.1). The guard's route wiring is untested.
D8.4 Hidden excluded saved work is absent only from resumption #3646, #3659 ReviewEligibilityPolicyTests::HiddenExcludedSavedWorkIsOnlyAbsentFromResumption; MongoT rows Done
D8.5 The reconciliation pool respects readiness; the action only warns #3646, #3659, #3663 ReviewEligibilityPolicyTests::ReconciliationReadinessGatesThePoolAndOnlyWarnsOnTheAction and 2 more Partial No UI shows the warning.
T.1 Converting a reviewed stage to Combined warns and needs explicit choices #3579, #3695 ReviewSettingsConsentPolicyTests (7); ApiT ::FirstScreeningSaveRecordsHistoryAndRequiresConversionConfirmation Partial NeverReviewed is never produced (MC.4), so every stage warns.
TT.1 Every decision appears in the truth table #3646 ::TruthTableCoversEveryActionAndReasonAndHasUniqueRows, ::MarkdownExportIsCurrent Done D2, D6, D7 and D3a are tested outside the table.
TT.2 Every row is tested against the pure policy #3646 ReviewEligibilityPolicyTests::PolicyMatchesTheTruthTable Done
TT.3 Every row is tested against the Mongo pools #3659 MongoT ::PoolPredicatesMatchTheTruthTableRow Partial Pool columns only (by design). Allocation is injected with applyAllocation:false, so the #3603 evaluator is not run per row.
TT.4 The save path agrees with the table #3691 ApiT ReviewWriteRefusalEndpointTests (refusals) Partial ActivityReviewWritePolicy is a separate implementation, and only its refusals are cross-checked, not its allowances.

2. UI/API agreement and the engineering contract

ID Requirement (doc line) PR(s) Test evidence Class Gap / note
AC.1 The server returns allowed/refused plus a reason per action (L1104) #3663 ApiT ReviewControllerTests.Eligibility::EveryStudyResponseAttachesEligibilityOnlyWhenThePolicyFlagIsOn Done
UI.1 (AC.2/AC.3/EC.14/EC.15) The UI shows server-permitted actions and derives no different rule (L1105-1107, L1164-1165) none none Not done The web never reads eligibility. screeningComplete re-derives #3551's veto and ignores Allow.
AC.4 Every save rechecks; page-open state never authorizes a save #3579, #3691 ReviewWriteRefusalEndpointTests::FlagOnRefusalIsTypedAndLeavesTheStudyAndProjectUnchanged; SubmitAnnotationSessionServiceReviewEligibilityTests::RetriesAFencedWriteThatLostItsRaceAndThenSaves Done
AC.5 A refusal keeps the draft #3579, #3691 Web review.effects.spec.ts "keeps the annotation draft…"; review-effects.decision-rollback.spec.ts Done
AC.6 / EC.22 Refusal, reconnect or configuration change refreshes eligibility and the study (L1111, L1179-1180) #3691, #3719, #3736 Web review-effects.decision-rollback.spec.ts "refreshes stage settings…" Partial Refreshes the stage and reviewer stats only, never eligibility or the study.
AC.7 Never auto-resubmit a refused decision #3579, #3691 Web review-effects.decision-rollback.spec.ts "…without offering a retry" Done
EC.1 A pure policy plus an orchestrator #3646, #3659 ReviewEligibilityServiceTests::EvaluatesThePolicyOverFreshlyReadFacts Done
EC.2 Reuse the presence/access result; no second lease model #3579, #3719 ActivityReservationAdmissionTests::AdmissionAcquiresIndependentClaimsAndRepeatOpenPreservesAge Done
EC.3 eligibility also on refresh and refusal responses (L1121-1122) #3663 – Partial The typed refusal problem carries only code and detail.
EC.4 Revisions and policy version #3663 ReviewControllerTests.Eligibility::EligibilityStudyRevisionMayBeNewer… Done The project revision stands in for the configuration revision.
EC.5 / EC.7 Separate per-action and per-selection decisions #3646, #3663 ReviewEligibilityPolicyTests (several) Done
EC.6 Stable reason codes and bounded details (L1129) #3663 – Partial No details field. Reasons serialize as integer ordinals, not names.
EC.8 Reservation ownership, timer and capacity facts in eligibility (L1132-1133) – – Partial Absent from the DTO. Only the access-result snapshot exists.
EC.9 Reopen and remove, including deletion's effect on claims #3646, #3691, #3719 ReviewControllerTests.RemoveSessionClaim::RemoveSession_WithThePolicyOn_… Done
EC.10 ReservationExpired, ConfigurationChanged, EligibilityUnavailable reasons (L1154-1158) – – Partial Not modelled (the doc admits this at L1226).
EC.11 / EC.12 403/404 for authorization, typed 409s; neutral screening-complete copy #3691 ReviewWriteAuthorizationResultHandlerTests; ReviewWriteRefusalEndpointTests Done
EC.13 Pools and policy share a table and fixtures #3646, #3659 as TT.2/TT.3 Done
EC.16 Local validity, busy and connection states compose with server eligibility, each with a visible reason (L1165-1167) pre-existing not verified Partial Nothing is composed with eligibility.
EC.17 / EC.18 Revalidate on every submission and retry; bind to versions at commit #3579, #3691 SubmitAnnotationSessionServiceReviewEligibilityTests (3) Done
EC.19 Tracking-mode admission, idempotency and authorization inside the fenced write #3579 ReviewControllerTests::SubmitScreening_FailsClosed_WhenTheDurableModeCannotBeVerified (not flag-on) Done, thin No flag-on test of the tracking-mismatch or reviewer-mismatch paths.
EC.20 Concurrent Stop screenings cannot both pass #3579 claims only Done, thin No two-writer screening-decision race.
EC.21 Allow has no hidden completion veto on the server #3646, #3579 ReviewEligibilityPolicyTests::CombinedAllowPermits…WithoutACapacityVeto Done Server only (UI.1).
EC.23 / EC.24 Coalesced, cancellable, trailing-refresh eligibility pipeline; re-evaluate transient refusals – – Not done
EC.25 Counters and statistics are not write authority #3579, #3695 ActivityReservationAdmissionTests::ScreeningSaveUsesCombinedPolicy… Done
RO.1 Direct access to a disabled stage or newly unavailable activity is refused (L452-456) #3691, #3663 hub and save tests Partial REST GetStudyForReview returns 200 with the study; the reconciliation GET has no refusal.
RO.2 Route guard for a disabled stage follow-on to #3659 Web stage-disabled-guard.service.spec.ts (8 cases) Done The wiring in stage.routes.ts is untested.
RO.3 Record-and-stay makes no new-study request pre-existing ReviewControllerTests::SubmitScreeningWithoutAdvancing_DoesNotAllocateNextStudy Done
WR.1 / WR.2 Pool membership is not permission; correction and reconciliation are exempt #3691, #3646 ActivityReservationAdmissionTests::AnnotationContinuationSurvivesFullTarget…; ReviewControllerTests::SubmitReconciliation_BypassesScreeningCapacityGuard Done
UI.2 Reason copy for every eligibility reason #3691, #3736 Web review.effects.spec.ts Partial Copy exists only for ReviewWriteRefusal codes and claim release.
UI.3 Refetch the study when eligibility.studyRevision is newer – – Not done

3. Minimum usable release, reservation lifecycle, revocation and the test plan

ID Requirement (doc line) PR(s) Test evidence Class Gap / note
MR.1 A single combined-only admin setting with D1/D2 (L1408-1411) #3579 StageReviewConfigurationTests (3); Web create-stage.component.spec.ts Done The grouped editor is S4-C.
MR.2 Strictness and Stop enforced on the API, including concurrent saves #3646, #3659, #3691 ReviewWriteRefusalEndpointTests Done, thin No race of two Stop votes (TP.C1).
MR.3 Matching UI and API behaviour (L1407-1409) #3663 server only Partial Same root cause as UI.1.
MR.4 Correct screening capacity and atomic predicate #3659, #3579, #3719 see SP.* Done, thin
MR.5 Independent claims, one timer, no single untyped slot #3579, #3719 ActiveReviewSessionTests::TypedFirstAnnotationSavePreservesScreeningClaimAndSharedTimer; PmT SessionTimeoutConsumerTests::TypedPage_… Partial Legacy untyped pages are only refused; see MG.11 for their drain.
MR.6 The impact check covers activity, strictness, targets and timing (L1417-1421) #3579 ::ClaimPlanningProjectionCarriesEveryFieldThePlannerAndCountsRead Partial Plan ignores idle-timeout changes and project-level changes (MR.13).
MR.7 A harmless edit is not blocked by presence #3579 – Done, thin No test with a live claim plus an unrelated edit.
MR.8–MR.10, MR.14, MR.15, MR.17 Settings conflict, Apply anyway, recompute and atomic commit; cancel and invalid leave state alone; editor UI #3579, #3695, #3719 ApiT StageReviewSettingsControllerTests (conflict, force, rollback, retain); Web review-settings.component.spec.ts, guarded-review-settings.spec.ts Done
MR.11 Step 4: durable intent, SignalR, clients refetch and disable only the affected action (L1442-1447) #3736 see RV.P* Partial No activity-specific disabling.
MR.12 A shared boundary between the settings commit and every claim or submit transition (L1449-1461) #3579 unconfirmed-save races only Partial No race of claims or submissions against a confirmed Apply anyway, nor of expiry, renew or convert.
MR.13 Project-scope impact when project-level rules change (L1459-1460) – – Not done Threshold or default-target changes, and membership disable, plan and revoke nothing.
MR.16 / RV.7 Completed-target annotation revocation; the saved-incomplete worked example #3579 screening only Done, thin "2 completed + 1 temporary, target 2" is untested. Q1.
RL.1–RL.8, RL.11, RL.13 Open, idle, dirty, clean, expiry, first save, submit, correct, multi-tab, delete-restore #3579, #3719 PmT SessionTimeoutConsumerTests::TypedPage_*, CheckConnectionLivenessConsumerTests; CoreT ActiveReviewSessionTests; ApiT NotificationHubStudyPresenceTests::OnDisconnectedAsync_TypedPage_… Done Some multi-tab and clean-restart tests use legacy pages.
RL.9 / TP.10 Clearing one's own decision with a conditional restore – – Deferred (out of MVP scope) The doc labels it a proposal; the lead ruled it out of MVP scope on 25 September 2026.
RL.10 / RL.12 Another change revokes only the ineligible claim; clients learn without waiting for a timer #3579, #3719, #3736 ::ConflictIsReadOnly… Partial Only two causes push to clients.
RV.1, RV.4–RV.6 Full capacity is not loss; a dirty draft doesn't veto; never delete saved work; earliest survivor #3579, #3646 ActivityReservationPolicyTests; ActivityReservationAdmissionTests::AnnotationContinuationSurvivesFullTarget… Done The tie-break is untested (D6.5).
RV.2 Screening completion removes screening claims and keeps annotation #3579 ActiveReviewSessionTests::ScreeningCompletionReleasesOtherStageScreeningClaimsButPreservesTheirAnnotationDrafts Partial Server release only: no intent or event.
RV.3 Loss of annotation eligibility removes the annotation claim #3719, #3579 ActivityReservationAdmissionTests::ExistingPageDoesNotBypassRevokedReviewPermission (asserts the page stays) Partial On permission or membership loss the claim is kept and only admission refuses it.
RV.8 Other revocation causes emit the event – the outbox has only two writers Not done Screening completion, permission or membership loss, and project threshold or target.
RV.P1 Atomic recheck on claims and submissions #3579, #3659, #3691 MongoT ::ConcurrentCrossStageClaims…; ApiT ::ClaimPinnedBeforeStageDisable… Done
RV.P2 Commit before publish, with durable intent #3736 MongoT ActivityClaimRevocationOutboxTests (3); CoreT ActivityClaimRevocationDispatcherTests Done Two causes only.
RV.P3 Cross-pod fan-out with delivery-time authorization #3736 ApiT ActivityClaimRevokedConsumerTests (5) Done, thin Authorization is tested. There is no two-pod fan-out test (statistics has PublishedInvalidationReachesTwoIndependentPodQueues).
RV.P4 Client refetch, per-activity disable, revision ordering #3719, #3736 Web study-presence.init.spec.ts; review.effects.spec.ts Partial A 30 s dedupe window only; no revision ordering and no per-activity disable.
RV.P5 Reconnect gets current state; no blind resubmit existing, #3719, #3736 Web signal-r.service.wiring.spec.ts (rejoin, typed denial) Done
SP.1, SP.2, SP.5, SP.8 Project/study-scoped screening count; annotation excluded; one predicate for selection and claim; Allow bypass #3659, #3579, #3646 CoreT ActivityReservationAdmissionTests::OutstandingScreeningAcrossOtherStages…; MongoT ::ScreeningClaimInAnotherStageLeavesNoPlaceForTheSameReviewer Done
SP.3 / TP.8 The minimum-2 five-row table (L894-921) #3659, #3579 ActivityReservationPolicyTests::ScreeningCapacityUsesSubmittedCompletion… (places only) Done, thin Rows with one outstanding claim are never run against the pool or admission.
SP.4 Manual-completion disagreement is excluded #3659 – Done, thin No manual-completion case.
SP.6 Two readers cannot take the one post-minimum place #3579 same-reviewer race only Done, thin No race between different reviewers.
SP.7 Configuration admission protects threshold, membership and policy #3579 stage-disable race only Partial No threshold or membership race.
TP.1, TP.2, TP.7, TP.11, TP.15b, TP.R1, TP.R2, TP.R5 Configuration variants; policy grid; claim policy; mode-change warning; stale action refused; rollout compatibility; statistics independence #3579, #3646, #3663, #3695 as cited in earlier sections Done
TP.1b, TP.R3, TP.R4 Migration, mixed-version and opt-in/rollback tests #3732 – S4-A/S4-B
TP.3 / TP.4 / TP.5 / TP.6 Target matrix ½/3; flag-on max-in-progress and saved fallback; "project count 2 / stage target 5"; enforcement × tracking grid #3659, #3579 partial Partial
TP.9 / TP.12 / TP.14 Timer lifecycle, settings layer, revocation delivery as above as above Partial Rolls up RL.10, MR.12, MR.13 and RV.8.
TP.C1 / TP.C2 Two final votes race, under Stop and under Allow – – Done, thin The fence exists but is untested.
TP.C3 / TP.C6 Configuration, threshold or membership change during a save; a version refusal causes a reread #3579 mocks; annotation path only Partial
TP.C4 / TP.C5 Slot expiry or capacity increase; source deletion races – – Not done
TP.13 UI/API parity, including a direct API bypass – – Not done Same as UI.1.
TP.15 Cross-pod recovery with two viewers; dropped events converge – – Not done
TP.0, TP.RB1–RB7 Staging setup and real-browser acceptance for the flag – e2e only covers unflagged #3551 Not done RB5 (Allow in the browser) would currently fail.
TP.AX The main acceptance example (L1536-1541) #3646, #3691 server only Partial The UI half fails under Allow.

4. Migration, activation and the writer floor

ID Requirement (doc line) PR(s) Test evidence Class Gap / note
AG.1 The flag defaults off, and off keeps existing behaviour #3579 StageReviewSettingsControllerTests::FlagOffDoesNotReadOrWriteReviewSettings Done See FO.* for byte identity.
AG.2 Flag off must not erase a persisted explicit policy (L40-41) #3579, #4336 ApiT StageReviewSettingsControllerTests::StoredStopIsKeptButNotAppliedWhileTheFlagIsOffAndIsEnforcedAgainWhenItReturns, ProjectControllerTests::FlagOffUpdateStageKeepsAStoredStopUnlessTheStageLeavesCombined; CoreT StageReviewConfigurationTests::LegacyUpdateThatStaysCombinedKeepsTheStoredPolicyByteForByte Done #4293: off keeps the stored Stop byte for byte and does not apply it (Chris's option B); leaving Combined clears it in both flag states.
AG.3 Don't enable before the contract and migration/writer gates are met (L42-43) – – Not done A plain boolean with no interlock. The gate is a roll-up of the rows below.
AG.4 / AG.6 / WS.1 Remaining activation work: settings reconciliation, restore, refusals, history proof, e2e, staging several see rows Partial
AG.5 Untyped reservations are untouched; typed admission refuses them #3579 ActivityReservationAdmissionTests::LegacyReservationIsATypedMigrationRefusalNotAnException; ApiT ::ForceDoesNotInterpretAnUnmigratedReservationAsTypedClaims Done
MG.1–MG.3, MG.5 Legacy mapping at read time #3579 StageReviewConfigurationTests Done
MG.4, MG.6 Production-getter semantics; the grouped destination #3732 #3732 tests S4-A
MG.7–MG.10, EV.1, EV.3 Dry run, versioned plan with before-image, idempotent apply, reconciliation, inventory – – S4-B
MG.11 Verify tracking history and outstanding untyped claims; drain or transition if any exist (step 7, L747-749) #3579 (refusal only) – S4-B Assigned to S4-B (lead, 25 September 2026).
MG.12 Seed HasEverBeenReviewed from positive tallies during migration (L1627-1631) – – S4-B Assigned to S4-B (lead, 25 September 2026). Safe default: shows the warning.
MC.1–MC.3, MC.5 Conversion consent, stage-scoped evidence, monotonic history, save-time recheck #3579, #3695 ReviewSettingsConsentPolicyTests, StageReviewStatisticsEvidenceTests, ApiT ::ConversionCannotCommitWithoutWarning… Done
MC.4 New or unused stages don't warn (L1607-1609) – pure test only Partial HasEverBeenReviewed=false is never recorded, even for stages created with the flag on, so they always warn.
MC.6 Combined → single-purpose keeps prior combined choices for a later return (L1655-1660) – – S4-C Assigned to S4-C (lead, 25 September 2026). ApplyReviewConfiguration clears the policy today.
WF.1 #3695 writer floor: no API/PM replica older than #3695 when enabling (L1681) #3695 StudyUpdateParseResultBsonTests (3) Not done (operator gate) Minimums are api-v9.141.0 and project-management-v11.137.0. The checkbox is unchecked and not enforced.
WF.2 Configuration-version plus per-service writer floor (L1680) #3732 ServiceVersionFloorTests, DatabaseMetadataWriterTests (#3732) S4-A/S4-B Concern for S4-A: connectedAt is written only at startup, and any pod's startup prunes versioned instances older than 24 h. A long-running below-floor pod can drop out of the registry, and the floor then passes wrongly.
WF.3 Client (web) floor: mixed clients never show Stop as enforced early (L1669-1670) – – Not done The web doesn't register in syrf_metadata.
WF.4, RB.1 Readers of both versions; old writers fenced; version-aware rollback #3732 #3732 tests S4-A/S4-B Checked and refuted: an audit agent's concern that pre-#3732 binaries can't load migrated projects. Stage : Entity<Guid> has [BsonExtraElements] ExtraElements, so unknown elements round-trip.
WF.5 No mixed writers that drop AdditionalScreeningPolicy (L99-100) #3579 ::NewCombinedStopSurvivesBsonRoundTrip Partial Met only if WF.1 is honoured.
RB.2 Rollback after an explicit Stop keeps the restriction (L1671-1673, L1702-1704) – – Not done Turning the flag off makes Stop stages behave as Allow, which the doc forbids. No minimum rollback writer is documented.
DS.1 Specify the four deployment states (L1685-1690) – – Not done
ST.1 Staging reviewer journeys before activation – – Not done Same as TP.0/TP.RB.
EV.2 Characterize production P against the implementation, and approve compatibility changes (L1785-1792) – – Not done

5. Allocation interaction (#2991, #3603, D8 decision 2)

ID Requirement PR(s) Test evidence Class Gap / note
AL.1 keepSlottedClaim=false is exactly the legacy rule #3719 ActivityReservationClaimReleaseTests::TheSlottedClaimExemptionAppliesOnlyWhenTheCallerOptsIn(false) Done
AL.2–AL.4, AL.7 ReviewController SubmitSession, direct access, SubmitAnnotationSessionService and the flagged save pass the flag only #3719 flag-off legacy tests (ReviewControllerAllocationTests, SubmitAnnotationSessionAllocationTests) Done, thin The legacy tests hold no typed claim, so they wouldn't catch a mis-wiring. No endpoint-level kept-claim continuation test.
AL.5 / AL.6 The settings plan and typed admission pass true or applyAllocation, reachable only under the flag #3719 ::FlagOffDoesNotReadOrWriteReviewSettings; ActivityReservationClaimReleaseTests Done, thin No test that the flagged repository entry points throw when the flag is off (FO.13).
AL.8–AL.12 Plan, ReviewersFor, AllocationClaimSlot, the release rule and continuation #3719 AllocationClaimSlotTests (5); ActivityReservationClaimReleaseTests; ReviewEligibilityPolicyTests (keep and release rows); MongoT release test Done
AL.13 Regime publication is refused while typed or untyped claims exist #3603 MongoT StageAllocationRegimePersistenceTests::BothActivityChecks_AgreeForEveryKindOfReviewActivity Done
AL.14 Flag off: #2991/#3603 Next, assign and filtering are unchanged #3579, #3659, #3719 ProportionalStageReviewServiceTests (13); StageReviewServiceReviewEligibilityTests::FlagOff_UsesTheLegacyStatusAndCandidateQueriesOnly Done

6. Flag-off identity (reviewEligibilityPolicy = false)

ID Changed path PR(s) Test evidence Class Gap / note
FO.1–FO.3 Unflagged: the reconcile grant (#3565), row ownership (#3671), and #3635 payload ordering with the query-parameter removal #3565, #3671, #3691 ReviewControllerReconciliationAuthorizationTests; ExtractionInfoSessionOwnershipTests; SubmitSessionOwnershipTests::FlagOff_… Done Sanctioned in the doc.
FO.4 Unflagged persisted-data change: ExtractionInfo.AddOutcomeData removes only this stage's rows (b667aa001) #3579 ActivityReservationAdmissionTests::ReplacingOutcomeDataInOneStagePreservesTheReviewersOtherStage Not done (unsanctioned) A data-preserving fix: other-stage outcome rows are no longer deleted. It needs a sanction line in the doc, not a revert.
FO.5, FO.8–FO.10, FO.12, FO.14, FO.16, FO.19, FO.20, FO.22, FO.23 Stage BSON null-omitted; hub flag-off branch verbatim; Refuse flag-off result; auth handler passthrough; the exception contract; guarded settings and AddStage; eligibility null (sanctioned); search import; Next legacy path; pure policy; idle dispatcher various as cited by each PR (e.g. ReviewWriteAuthorizationResultHandlerTests::OtherwiseTheDefaultHandlerForbidsAsBefore, StageReviewServiceReviewEligibilityTests::FlagOff_*, ActivityClaimRevocationDispatchServiceTests::FlagOff…) Done
FO.6 A legacy SlotReservation BSON lacks the new elements #3579 – Done, thin Identical by inspection only.
FO.7 Legacy domain paths acting on typed pages after a flag on→off rollback #3579 – Done, thin Identical while no typed pages exist. Rollback behaviour ties into RB.2.
FO.11 The detail of the unflagged AnnotationOwnershipMismatch 403 changed (#3691 after #3671) #3691 – Partial The code and status are unchanged.
FO.13 The flagged repository entry points throw when the flag is off #3579, #3695 admin batch only (AdministrativeScreeningBatchTests::TheFencedBatchRefusesToRunWithThePolicyOff) Done, thin No guard test for TryAdmit…, TrySaveActivityReview… or TrySaveActivitySessionDeletion….
FO.15 Wire: StageDto.additionalScreeningPolicy serializes as null (Newtonsoft includes nulls) #3579 – Not done (unsanctioned)
FO.17 Wire: claimRelease: null on NextStudyResponseDto and StudyReviewAccessResult (REST and SignalR) #3719, #3736 – Not done (unsanctioned)
FO.18 Wire: bulk-update DTO gains refusedRows: 0, refusedScreeningRows: 0 and refusedScreeningRowSample: null #3695 BSON is proven identical (StudyUpdateParseResultBsonTests::AFlagOffResultSerializesWithoutTheRefusalReport) Partial JSON is not identical.
FO.21 Shared additive members (GetUncachedOrDefaultAsync, ActivityReviewAdmission.Admitted, StageAllocationAdmission.Plan) #3659, #3719 callers Done, thin
FO.24 Infrastructure: the pmActivityClaimRevocationOutbox collection and indexes are created by PM index-init, and each API pod declares a temporary api-activity-claim-revocations endpoint #3736 – Not done (unsanctioned) Inert while the flag is off. Sanction it in the doc, or gate it.
FO.25 Web flag-off behaviour several Web specs Done, thin Only the sanctioned codes reach the UI when the flag is off.

Slice plan (lead, 25 September 2026)

Slice Owner Scope
S6a s1b-server G3 server-contract residue and G5 flag-off identity (sanction lines, the AG.2 test), plus the D6.6 interpretation note. Merged before S6b.
S6b s1b-server, after S6a G1: the reviewer browser consumes eligibility. A plan goes to the lead first.
G2 new worker, in parallel Revocation coverage for the remaining causes.
G4 each slice adds the races in its own area Leftovers go to a final hardening slice.
G6 S4 worker, after S4-C Activation readiness.
MG.11, MG.12 S4-B Untyped-reservation inventory and drain; history-marker seeding.
MC.6 S4-C Keep prior combined choices for a later return.

Gaps not owned by S4 and not deferred

These are grouped as candidate slices. The IDs refer to the tables above.

G1. The reviewer browser consumes eligibility (largest; blocks D1/Allow in the UI)

UI.1 (AC.2/AC.3/EC.14/EC.15/MR.3/TP.13), D8.5 (ReconciliationNotReady warning), UI.2 (reason copy), UI.3 (stale study refetch), AC.6/EC.22 (refetch eligibility and the study after a refusal, reconnect or change), EC.23/EC.24 (coalesced refresh pipeline; re-evaluate transient refusals), RV.P4/MR.11 (per-activity disable, revision ordering), EC.16 (composing local states), RO.2 route-wiring test.

G2. Revocation coverage for the remaining causes

  • RV.8/RV.2: screening-completion releases emit intents and events.
  • RV.3: permission or membership loss revokes the claim, rather than only refusing admission.
  • MR.13/SP.7: project threshold or default-target changes and membership disable plan and revoke affected claims.
  • MR.6: idle-timeout changes enter the impact plan.
  • RL.10/RL.12 roll up into these.

G3. Server-contract residue

  • RO.1/D8.3: REST GetStudyForReview and the reconciliation GET refuse a disabled stage.
  • EC.3: eligibility on refusal responses.
  • EC.6: string reason codes and bounded details.
  • EC.8: reservation, timer and capacity facts.
  • EC.10: the ReservationExpired/ConfigurationChanged/EligibilityUnavailable codes.

G4. Concurrency and test-matrix hardening

  • Races: D6.4/MR.12 (confirmed Apply anyway, expiry, renew and convert), TP.C1/TP.C2 (two final votes), TP.C3/TP.C6 (threshold or membership change mid-save; screening reread), TP.C4/TP.C5, SP.6, EC.20, TP.15 (two-viewer cross-pod).
  • Capacity and revocation cases: SP.3/TP.8 (outstanding-claim rows through pool and admission), SP.4 (manual completion), D6.5 (tie-break), MR.16/RV.7 (completed-target annotation; saved-incomplete).
  • Missing tests:
  • MR.7 (harmless edit);
  • D5.2 (grouping round-trip);
  • EC.19 (flag-on tracking or reviewer mismatch);
  • TP.3/TP.4/TP.5/TP.6 matrices;
  • TT.3 (a real allocation evaluator per row);
  • TT.4 (the save path's allowances vs the table);
  • AL.2–AL.7 (endpoint-level kept-claim continuation);
  • RV.P3 (two-pod fan-out);
  • FO.6 (legacy SlotReservation BSON);
  • FO.13 (flag-off guards on the repository entry points).

G5. Flag-off identity

FO.4, FO.11, FO.15, FO.17, FO.18 and FO.24. Each needs either a sanction line in the policy doc's flag decision, or a fix: null-omit the new DTO members, or gate the index-init and endpoint registration on the flag. AG.2's flag-off test that a stored Stop survives a legacy update landed in #4336 (#4293).

G6. Activation and rollout, not in S4's stated scope

  • AG.3: an activation interlock, or at least a runbook gate.
  • WF.1: verify the replica floor at enable time.
  • WF.3: a web client floor.
  • WF.5.
  • RB.2: a rollback rule after an explicit Stop, plus the minimum rollback writer.
  • DS.1: specify the four deployment states.
  • ST.1/TP.0/TP.RB1–RB7: e2e and staging acceptance for the flag.
  • EV.2: characterize production P and record approved compatibility changes.
  • MC.4: record NeverReviewed for stages created under the flag.

Scope questions for the lead (possibly S4-B)

  • MG.11: an untyped-reservation inventory and drain.
  • MG.12: seeding the history marker.
  • MC.6: keeping combined history for a later return.

Concern to relay to S4-A (#3732)

WF.2: the version floor can pass wrongly once any pod's startup prunes a long-running versioned instance, because connectedAt is written only at startup.

Decision questions (answered 25 September 2026)

# Question Decision Rows
Q1 D6 completed-target revocation: the code skips it when the annotation target is unenforced, and counts allocated incomplete sessions. Which is intended? Keep the code. An unenforced target always leaves a place, so revoking would contradict the approved AnnotationPlaceAvailable semantics, and counting allocated incomplete sessions matches the capacity model. The interpretation note is recorded in the policy doc by S6a. D6.6, MR.16
Q2 D3a fixed-two summary discrepancy in the MVP? Pending the lead. It is display only: StudyStats hardcodes minNumberSessions = 2 for the stage-progress annotation counters and their materialized mirror, so they miscount on stages whose target isn't 2. Selection, admission and saves use the real target. D3a.2
Q3 Defer D8(2) "under reallocation"? Yes, to allocation Phase 3: #3745. D8.2c
Q4 Is "clear own decision" in scope? No. Out of MVP scope, recorded as deferred. RL.9, TP.10
Q5 Owner of MG.11, MG.12 and MC.6? MG.11 and MG.12 go to S4-B; MC.6 goes to S4-C. MG.11, MG.12, MC.6