Skip to content

Reviewer screening rebuild and invalidation

This first Phase 4A slice adds rebuildable membership-screening and reviewer-screening rows, authorized current selectors and shared-root historical observations. Existing consumer endpoints continue to use their authoritative queries. This is a correctness delivery, not a read-performance cutover or completion of incremental Phase 4A maintenance.

Formula compatibility

Both families use ForMembership(InvestigatorId) scopes, including members with no screenings. They remain separate metric families: the membership aggregation uses live agreement measures, while the reviewer query uses persisted InclusionInfo at the configured threshold. Missing persisted inclusion entries need not enter either availability bucket. These answers are not interchangeable.

Membership counters preserve the current value-object mapping, including project-wide sufficiency and overscreening values, the duplicated excluded-agree value in both agree outputs, and availability computed by subtracting reviewer counts from project counts. Percentages remain derived by the existing value object, including its truncation and zero-total rules. Catalogue corrections require a separate deliberate migration; rebuilding never silently corrects legacy formulas.

The rebuild reads Project, source counts and revision controls in one pinned snapshot. Membership uses the production FullStats pipeline; reviewer counts reuse the production persisted-inclusion filters, executed sequentially through the same Mongo session. Removed membership scopes return an authoritative absence rather than a fabricated zero row.

Mutation and rebuild contract

Ordinary screening transactions invalidate both reviewer families, including nonparticipants whose availability or project-wide totals changed. A MarkFamilyStale dependency advances the family's write epoch and marks its guard Stale in the source transaction. Work is constant per family rather than proportional to membership size. Abort rolls back this invalidation; receipt replay does not advance the epoch again. Rebuilding one member cannot revive older untouched member rows because their applied epochs still disagree with the guard. An active durable fence remains in force.

Reviewer-family enablement also admits the screening writer when project-screening serving is off; that route performs source-only invalidation without publishing disabled project counters. Existing population/import/delete and inclusion-recalculation operations fence all canonical member scopes, then use the registered authoritative family calculators during rebuild. They retain their durable operation ownership and transaction boundaries.

Administrative path and permissions

With writes, the existing membership-screening family flag and the reviewed project allowlist enabled, a batch-project administrator can run:

  • POST /api/admin/project-statistics/{projectId}/membership-screening/rebuild
  • POST /api/admin/project-statistics/{projectId}/reviewer-screening/rebuild

These synchronous triggers reuse existing restartable backfill/checkpoint orchestration and typed contention outcomes. Each enumerates current canonical members, records the observed bootstrap checkpoint, and rebuilds each row. New families use distinct backfill-observed:MembershipScreening:{scope-set-digest} and backfill-observed:ReviewerScreening:{scope-set-digest} triggers, so a project's older screening bootstrap cannot masquerade as their first observation. A repeat for the same exact canonical scope set reuses its own original history point. Adding or removing members changes the scope-set digest and creates a newly observed root; it cannot report a false NoOp using a root that never covered the requested scope set. Earlier immutable roots retain their truthful gaps for scopes they did not observe. Partial work reports incomplete and can be retried. The existing forced-rebuild/retry guidance applies if a different checkpoint occupies the bootstrap identity.

Ordinary current and historical selectors retain their existing graph permissions, active own-row membership requirements, and additional ViewMemberships permission for peer rows. Historical multi-member results resolve one immutable checkpoint root. No cross-root composition is added.

Required next slice before Phase 5.3 activation

Ordinary screening commits intentionally cause authoritative fallback until rebuild. Before moving reviewer consumers to materialized reads, implement and measure sustainable maintenance: bounded incremental/derived updates or bounded automatic rebuild scheduling, including large memberships, corrections, resets and population changes. That work is required for Phase 5.3 activation, not an optional performance improvement. Keep consumer flags disabled until its parity and workload gates pass. Kappa, search families and reporting adapters remain their own planned slices.

Administrative routes

Each reviewer family exposes both an idempotent .../{family}/backfill and a forced .../{family}/rebuild, mirroring the project-screening pair. The distinction is not cosmetic: forcing is the reconciliation that adopts the authoritative configuration identity inside the publication's own control compare-and-swap and republishes every scope, so it must not be a family's only entry point. The non-forced pass is the ordinary operational trigger.

Administrative responses identify the selected metric family and expose the same typed 202 summary, 409 problem details and 404 responses in the generated client. A repeated non-forced backfill selects unchanged lower-generation rows exactly as the current reader does — the exact row at the guard's visible generation when it exists, otherwise the greatest published row below it — so rebuilding multiple reviewers does not churn a revision on every repeat. A publication that did not happen is reported with the disposition that matches what actually happened to the row: Stale when the rebuild ran and returned the scope to Stale, Absent when the family says the scope does not exist, and Contended when the rebuild never started — another owner holds the live lease or the guard's sole candidate slot, or a fence covered the family before a candidate was claimed — so the published row is untouched and still serving. All three are partial failures that answer 409, but only Stale means the projection degraded, and ScopesStale is the number an operator reads to decide whether to escalate. Lease contention does not imply that an existing Fresh row was changed.

Deferred to the read-side parity slice

Per-family IProjectStatisticsAuthoritativeCalculator implementations for the two reviewer families are deliberately not in this slice. That contract's only consumer is ProjectScreeningParityAuditService, which takes exactly one calculator, so wiring reviewer families into shadow parity needs a per-family audit consumer — design work belonging with the reviewer consumer cutover, not with rebuild and history. Adding unregistered implementations ahead of it would be code nothing resolves and nothing calls.