Recovered Statistics Design Reconciliation¶
Purpose and evidence¶
This is the decision ledger between the March-April 2026 statistics design and the current materialized-project-statistics programme. It prevents the older work from being reduced to a broad idea or the newer work from silently replacing decisions that were already supported by code-level analysis.
Primary recovered evidence:
- docs/planning/mongodb-stats-precalculation-plan.md from pull request #2534;
- docs/planning/stats-precalculation-master-plan.md;
- implementation plans M008 through M011, including every slice plan;
- the authoritative StudyStats aggregation on current main;
- current mutation, membership, question, import, reservation, API and SignalR paths; and
- the preserved screening candidate from pull request #2985.
The old branch is evidence only. File and handler names in it are not assumed current until Phase 0 reconfirms them on main.
Disposition vocabulary¶
- Restore earlier: the earlier decision is the preferred current design.
- Combine: retain the proven earlier mechanism and add a newer safeguard or broader contract.
- Retain newer: the newer plan closes a real gap without discarding valid earlier evidence.
- Reject earlier: the earlier decision is invalid or unsupported and must not return.
Recovered decision register¶
The master plan numbered thirteen decisions. Their current dispositions are:
| Earlier decision | Disposition | Current decision |
|---|---|---|
| D01: separate pmProjectStats collection, not fields embedded on Project | Combine | Preserve a separate disposable read model, but use natural scoped-current, delta, checkpoint and control collections rather than one payload row. |
| D02: Study mutation methods return DDD delta value objects | Restore earlier | Before/after transition value objects remain the preferred ordinary path; repository mapping owns physical field paths. |
| D03: Study and statistics delta commit in one transaction | Restore earlier | Add the immutable unique delta/provenance record to the same transaction. |
| D04: store settings-independent tally dictionaries | Restore earlier | Preserve raw tallies where query parity proves them; derive configuration-dependent totals instead of duplicating them. |
| D05: initial project/pairwise kappa scope | Reject earlier | The stale implementation's cross-stage cohort is invalid. Raw decision profiles remain; any kappa needs a separate same-stage specification. |
| D06: Legacy to Backfilling to Live with a short write freeze | Combine | Use Missing/Stale/Rebuilding/Fresh, scoped fences and authoritative fallback. A measured short gate is last resort when snapshot boundary plus delta replay cannot be proven. |
| D07: tracking/sentinel document in the stats collection | Reject earlier | Use typed control, immutable delta provenance and checkpoint-run evidence; no magic date sentinel or growing mutable failures array. |
| D08: two-phase import and deletion | Combine | Retain PendingImportJobId/PendingDeletion and signed bulk deltas; make import visibility one Project-level hidden token rather than per-row clearing, and add stable operation/fence/progress plus an authoritative-rebuild escape hatch. |
| D09: three-source audit and repair | Combine | Keep materialized versus legacy facets versus independent streamed classification at one boundary; repair only through fenced evidence-bearing rebuild. |
| D10: sparse daily snapshots with tracking metadata | Combine | Keep sparse real observations and explicit gaps; add fixed-size checkpoint roots over bounded pages that may reference prior unchanged immutable observations, revision/provenance and bounded retention/history. |
| D11: state-aware current, history and realtime reads | Combine | Use one authorized catalogue query, coherent requested-bundle fallback and revision/scope SignalR invalidation/refetch. |
| D12: delete statistics on Project deletion | Combine | Deny/tombstone immediately and purge current/history under the Project's approved retention policy. |
| D13: question deletion resets affected annotation sessions/statistics | Combine | Preserve exact completed-to-incomplete bucket moves and definition identity; fence/rebuild only when the rewrite is unbounded or non-additive. |
M008-M011 implementation-plan crosswalk¶
| Recovered slice | Disposition | Required successor evidence |
|---|---|---|
| M008 S01 screening/annotation classification value objects | Restore earlier | Truth tables and real query fixtures must match all applicable current facets, including exact boundaries. |
| M008 S02 ScreeningDelta/AnnotationDelta, rescreening old-state capture and inverse property | Restore earlier | Signed old-bucket decrement/new-bucket increment, zero-delta and inverse tests remain mandatory. |
| M008 S03 pmProjectStats repository and ReviewDomainService transaction | Combine | Retain transactional orchestration, replace one large current document with natural scopes and add unique immutable delta history/retry safety. |
| M009 S01 staged two-phase import | Combine | Retain invisible staging and bounded atomic reveal/bulk delta; add durable operation fencing, restart progress and measured transaction limits. |
| M009 S02 pending two-phase deletion | Combine | Retain batched classify + delete + negative delta transactions; hold fence until final parity/reconciliation. |
| M009 S03 remaining session/reconciliation/membership/question mutation coverage | Combine | Re-audit current main for renamed/new owners, including durable SlotReservation effects, then use signed deltas where supported. |
| M009 S04 settings reclassification from tallies | Restore earlier | Recompute from approved settings-independent tallies when bounded; rebuild only if required source facts are absent. |
| M009 bootstrap slice | Combine | Prefer source boundary plus committed-delta replay and authoritative fallback over a mandatory review freeze. |
| M010 S01 replace aggregation read and remove sync-over-async | Combine | Cut over one authorized consumer at a time after parity; keep compatible DTO adapters until all requested families exist. |
| M010 S02 three-source audit | Combine | Preserve exact legacy-query and streamed-classifier sources; add scoped leases, immutable mismatch evidence and non-blind repair. |
| M010 S03 stats change-stream full push | Retain newer replacement | Emit client-invalidation revision/family invalidation after commit with source/projection provenance and refetch through authorization; do not push stored supersets. |
| M011 S01 sparse daily snapshot | Combine | Keep truthful sparse observations, key checkpoints by the three-component identity (source revision, projection revision, mode epoch) so any kind of change can append, and add bounded retention. |
| M011 S02 history endpoint and failure metadata | Combine | Use the shared current/history catalogue contract, typed unavailable/410 results and current authorization. |
| M011 S03 area-chart activation and fill-forward | Retain newer phasing | Backend history is required; visible UI is a later independently flagged consumer and may render explicit gaps rather than silently masking failures. |
Mutation ownership recovered from the earlier plan¶
These are evidence anchors, not assumed-current implementation names. Phase 0 must re-resolve every row against current main before assigning file ownership. Every source-changing row also commits the bounded all-mode source-operation receipt and client-invalidation outbox entry; the table below names its family-specific projection action:
| Mutation | Earlier/current evidence anchor | Required materialized action |
|---|---|---|
| Submit/correct/rescreen | ReviewController submission paths and ReviewSubmissionService.AddScreening | Transactional source + project/reviewer signed screening moves + delta record |
| Candidate annotation save/complete | ReviewController.SubmitSession and ReviewSubmissionService.AddSessionData | Transactional stage/reviewer/question signed moves where catalogue-supported |
| Domain reconciliation session lifecycle | Session save/complete/delete paths catalogued in M009 | Transactional stage/reviewer reconciliation moves; this is not parity repair |
| Threshold/agreement change | ProjectAgreementThresholdUpdatedHandler | Reclassify from screening tallies when bounded, otherwise fence/rebuild |
| Stage/session settings change | Project/stage aggregate replacement handlers | Reclassify from session tallies when exact; otherwise fence affected scopes |
| Systematic-search import | Earlier SearchImportJobConsumer, now requiring current-owner resolution | Two-phase staged invisibility plus bounded bulk delta or fenced rebuild |
| Systematic-search deletion | ProjectManagementService.DeleteSearchStudies | PendingDeletion, delete + negative delta batches, final parity before fence clear |
| Question deletion | ProjectManagementService.DeleteQuestionAsync and ResetSessionsForStagesAsync | Preserve completed-to-incomplete reset semantics and question/stage invalidation |
| Membership add/disable/remove | Project membership lifecycle, including later DisableMembership behavior | Current authorization immediately; bounded reviewer-scope delta/invalidation or fenced non-additive rebuild |
| Durable reviewer reservation lifecycle | Later SlotReservation claim/release/expiry repositories and consumers | Include only if a current approved availability/capacity formula uses durable reservations |
| Project deletion | Project deletion owner resolved in Phase 0 | Deny/tombstone immediately, then retention-policy purge |
| Legacy manual question tally refresh | Existing Project tally refresh path | Remains authoritative until the question-family cutover, then is superseded |
Authoritative formula and facet baseline¶
The earlier documents describe an 11-facet query, but current main has 15 facets. The catalogue must name and fixture every current facet before replacing it:
| Current facet | Meaning retained for parity |
|---|---|
| Investigator | Investigator-level screening facts used by membership profiles |
| All | All available studies in the project population |
| UnexcludedInvestigatorStats | Per-investigator annotation values for the unexcluded population |
| ExcludedInvestigatorStats | Per-investigator annotation values for the excluded population |
| AllUnexcluded | Project/stage annotation values for the unexcluded population |
| AllExcluded | Project/stage annotation values for the excluded population |
| UnexcludedSessioned | Unexcluded studies with stage session state |
| ExcludedSessioned | Excluded studies with stage session state |
| UnexcludedCandidateSessionsStartedGroupedCount | Candidate-session started tally for unexcluded studies |
| UnexcludedStartedReconciliationCount | Reconciliation-started tally for unexcluded studies |
| UnexcludedCompletedReconciliationCount | Reconciliation-completed tally for unexcluded studies |
| ExcludedCandidateSessionsStartedGroupedCount | Candidate-session started tally for excluded studies |
| ExcludedStartedReconciliationCount | Reconciliation-started tally for excluded studies |
| ExcludedCompletedReconciliationCount | Reconciliation-completed tally for excluded studies |
| ScreeningGroupedCount | Count by number of screenings and number of include decisions |
The consumer-visible per-stage AnnotationSessionStats does not take its reconciliation counts from the
standalone reconciliation facets above: CreateAnnotationSessionStats maps each stage's started and
completed reconciliation values from that stage's Sessioned.CompletedAndStartedReconciliation and
Sessioned.CompletedAndCompletedReconciliation. Phase 3 must fixture a multi-stage project with unequal
per-stage reconciliation counts, so an implementation cannot assign project-wide reconciliation totals
to every stage while still matching the standalone facets.
The current screening equations to preserve exactly until a separately approved semantic correction are:
- number-screened sufficiency: NumberScreened is greater than or equal to the configured NumberScreened threshold;
- agreement sufficiency: AbsoluteAgreementRatio is strictly greater than the configured ratio, or strictly greater than -1 when the threshold is absent;
- sufficiently included: both sufficiency predicates and Inclusion strictly greater than 0.5;
- sufficiently excluded: both sufficiency predicates and Inclusion strictly less than 0.5;
- Inclusion exactly 0.5 is in neither included nor excluded under the current query;
- started: NumberScreened strictly greater than zero;
- overscreened: NumberScreened strictly greater than the current hard-coded lower bound of 3;
- overscreened-yet-insufficiently-screened: NumberScreened strictly greater than 3 and AbsoluteAgreementRatio strictly less than the configured ratio (or less than -1 when absent). This is not the logical intersection of Overscreened and InsufficientlyScreened: equality with the agreement threshold increments InsufficientlyScreened but not this composite facet, and this facet does not test the configured NumberScreened sufficiency threshold;
- overscreened-and-sufficiently-included/excluded: NumberScreened strictly greater than 3, AbsoluteAgreementRatio strictly greater than the configured ratio (or greater than -1 when absent), and Inclusion respectively strictly greater than or strictly less than 0.5. These composite facets do not test NumberScreened greater than or equal to the configured sufficiency threshold, so a Study above 3 screenings but below a higher configured threshold can increment one of them while remaining InsufficientlyScreened; and
- ScreeningGroupedCount key: (NumberOfScreenings, IncludedCount), retaining valid mixed include and exclude decisions from different reviewers.
Phase 2 project-level query parity fixtures must lock these legacy asymmetries explicitly: agreement ratio exactly equal to its threshold, and NumberScreened above 3 but below a configured threshold greater than 4. Phase 4 applies the equivalent fixtures to membership/reviewer screening facets when that family is introduced. They remain compatibility facts, not approval of the labels or a semantic correction.
The current annotation query uses a hard-coded minimum of two candidate sessions
(minNumberSessions = 2, StudyStats.cs) in exactly ten predicates even though a stage has
SessionCountTarget. SessionedGroupStage applies it to Available, Full, Completed,
CompletedAndNotStartedReconciliation, CompletedAndStartedReconciliation,
CompletedAndInProgressReconciliation and CompletedAndCompletedReconciliation;
InvestigatorSessionGroupStage applies it to NumberOfStudiesSessionAvailable,
NumberOfStudiesSessionFull and NumberOfStudiesReconcileSessionAvailable. That is a parity fact and an
unresolved product semantic, not permission to substitute the target during materialization. The
catalogue must first decide whether to preserve or separately correct it, and the Phase 3 parity fixtures
must assert each of these ten fields independently on stages whose SessionCountTarget is 1 and 3, so an
implementation cannot use the configured target for some of them while passing the generic lifecycle
cases.
The current membership/reviewer mapper uses project-wide allInfo.All values, rather than the
investigator facet, for SufficientlyIncluded, SufficientlyExcluded, SufficientlyScreened,
InsufficientlyScreened, Overscreened, OverscreenedYetInsufficientlyScreened,
OverscreenedAndSufficientlyIncluded and OverscreenedAndSufficientlyExcluded. It also maps the
project-wide denominator itself: TotalNumberStudies is project-wide allInfo.All.Count, and every
membership screening percentage (PercentIncluded, PercentExcluded, PercentScreened,
PercentAvailable, PercentUnavailable) and the NumberVerification balance predicate divide or
balance against that project-wide total, never a per-reviewer count. Each percentage is truncated, not
rounded: the current value is Math.Truncate(10000 * numerator / TotalNumberStudies) / 100, so a
non-terminating ratio keeps its two-decimal floor (one of six is 16.66, never 16.67) and a raw
decimal or ordinarily rounded result would break current-response parity. It also passes the
investigator facet's SufficientlyExcludedAndAgree value into both the
SufficientlyIncludedAndAgree and SufficientlyExcludedAndAgree output fields. The remaining
membership screening outputs — Screened, Included, Excluded, SufficientlyIncludedAndDisagree and
SufficientlyExcludedAndDisagree — map directly from the investigator facet, and Phase 4 fixtures must
include unequal project/investigator values for these direct mappings too, so a replacement can neither
source them from project-wide totals nor recompute them differently while passing the exceptional-case
fixtures. Its availability outputs
are subtraction formulas rather than direct facet aliases:
Available = allInfo.All.InsufficientlyScreened - investigator.InsufficientlyScreened; andUnavailable = allInfo.All.SufficientlyScreened - investigator.SufficientlyScreened.
Phase 4 must preserve and fixture all of these mappings with unequal project/investigator,
included-agree/excluded-agree, and availability-subtraction source values — including fixtures where the
project-wide allInfo.All.Count total differs from the reviewer's own screened/available counts, so that
a per-reviewer denominator would visibly change TotalNumberStudies and every derived percentage, and a
non-terminating-ratio fixture for each derived percentage that would fail under rounding or untruncated
output — until
a separately approved semantic correction changes the live contract; intuitively substituting
reviewer-specific, direct or one-to-one values would not have current-response parity.
The following stored, settings-independent tallies from the earlier plans remain the preferred raw inputs where current-query parity confirms them:
- screeningTallyCounts[numberOfScreenings][includedCount];
- candidateSessionsCountLookup[stage][candidateSessionCount][completedSessionCount], preserving the
authoritative per-Study joint distribution rather than collapsing Studies with equal candidate counts.
The grouped facet contains only Studies with at least one candidate session; the current mapper
explicitly appends the stage
(0 candidate, 0 completed)bucket astotalCount - sessioned.Count, separately for the unexcluded and the excluded population, each with its own total and sessioned facet. Phase 3 must record this subtraction as its own mapping and fixture it for both populations with non-zero sessioned counts, rather than deriving the no-session bucket from the grouped tally or from the wrong population; - candidate and domain-reconciliation started/in-progress/completed transitions by stage and inclusion class; and
- membership-scoped equivalents needed to derive currently authorized available, unavailable, completed and agreement views.
The current CreateMembershipAnnotationStats mapper applies the following formulas independently to the
unexcluded and excluded populations, with that population's allCount, aggregate sessioned facet and
membership/stage investigator facet:
Available = allCount - sessioned.Count + sessioned.Available - investigator.NumberOfStudiesSessionAvailable;InProgress = investigator.NumberOfStudiesSessionInProgress;Completed = investigator.NumberOfStudiesSessionCompleted;Unavailable = sessioned.Full - investigator.NumberOfStudiesSessionFull;Total = allCountandCandidateAnnotationSessionsGloballyCompleted = sessioned.Completed;ReconcileAvailable = sessioned.CompletedAndNotStartedReconciliation - (AllowSelfReconciliation ? 0 : investigator.NumberOfStudiesReconcileSessionAvailable);ReconcileInProgressandReconcileCompletedare the matching investigator reconciliation counts; andReconcileUnavailable = AllowSelfReconciliation ? 0 : investigator.NumberOfStudiesReconcileSessionAvailable, withSelfReconciliationEnabled = AllowSelfReconciliation.
The outer membership-stage envelope additionally maps two fields of its own: Total = allInfo.All.Count
— the project-wide population, not either population-specific allCount used by the inner formulas
above — and IncompleteThreshold = stage.MaxInProgress, passed through even when unset. Phase 3 must
fixture a configured MaxInProgress and a project-wide total that differs from both the unexcluded and
excluded population counts, so a replacement can neither derive the outer total from the wrong
population nor drop the threshold while passing the inner parity cases.
The stage capacity flags are derived from these population stats by the Stage aggregate, not stored:
HasReachedMaxInProgress is false when MaxInProgress is null or non-positive, and otherwise true
exactly when the counted in-progress value is greater than or equal to MaxInProgress, where the
counted value is the unexcluded in-progress count alone when HideExcludedStudiesFromReviewers is true
and the sum of the unexcluded and excluded in-progress counts otherwise.
HasReachedMaxInProgressReconciliation applies the same boundary and the same conditional population to
the reconciliation in-progress counts.
Missing facets contribute zero exactly as the nullable current mapper does. Phase 3 must fixture unequal
project/session/investigator values, both AllowSelfReconciliation states, and — for both capacity
flags — both HideExcludedStudiesFromReviewers states with in-progress counts one below, exactly at and
one above the MaxInProgress boundary plus the null no-limit case; replacing these formulas
with intuitive direct per-reviewer availability or reconciliation counts is a semantic change outside this
programme.
The consumer-visible MergedMembershipAnnotationStats then combines the unexcluded (U) and excluded
(E) results according to ExcludedSessionStatsGrouping (G) with another non-intuitive compatibility
mapping:
Available = U.Available;InProgress = U.InProgress + (G == WithUnexcluded ? E.InProgress : 0)andCompletedfollows the equivalent formula;Unavailable = U.Unavailable + E.Unavailable + E.Available + (G == Unavailable ? E.InProgress + E.Completed : 0);Total = U.Total + E.Total, and candidate sessions globally completed sum both populations;ReconcileAvailable = U.ReconcileAvailable;ReconcileInProgressandReconcileCompletedadd their excluded value only forWithUnexcluded;ReconcileUnavailable = U.ReconcileUnavailable + E.ReconcileUnavailable + E.ReconcileAvailable + (G == Unavailable ? E.ReconcileInProgress + E.ReconcileCompleted : 0); andSeparateadditionally exposes the excluded candidate and reconciliation in-progress/completed values in its separate block instead of folding them into the merged progress/completion fields.
Phase 3 must fixture all three WithUnexcluded, Unavailable and Separate modes with non-zero, unequal
excluded values. Excluded availability contributes to merged Unavailable in every mode; changing that
behavior requires a separately approved live-contract correction.
The same value object additionally derives consumer-visible fields at read time rather than storing
them: InsufficientlyCompletedCandidateSessions = Total - CandidateAnnotationSessionsGloballyCompleted,
and eight percentage outputs (PercentAvailable, PercentInProgress, PercentCompleted,
PercentUnavailable and their four Reconcile equivalents) computed as
Total == 0 ? 0 : Math.Truncate(10000 * value / Total) / 100 — truncation to two decimal places, never
rounding, with a zero total mapping to zero. In the Separate grouping, the separate excluded block's
percentages (PercentExcludedInProgress, PercentExcludedCompleted and their two reconciliation
equivalents) divide the excluded-only counts by the merged overall Total = U.Total + E.Total, not the
excluded population's own total. Phase 3 fixtures must include non-terminating ratios (for example one
of six yielding 16.66, not 16.67) and unequal unexcluded/excluded totals proving the merged-total
denominator for every separate-block percentage and the derived insufficiently-completed subtraction.
These are compatibility facts, not approval of the denominators or the truncation.
Derived values should be calculated from those stable tallies at query time when that avoids duplicate counters. A derived field may be stored only after measurement demonstrates that derivation itself is materially expensive.
Decision-ready material differences¶
| Area | Recovered earlier decision | Current-plan divergence | Disposition and reason |
|---|---|---|---|
| Primary update path | Ordinary screening and annotation writes return signed before/after deltas and apply targeted MongoDB increment/decrement operations in the same transaction as Study | Every mutation emits an outbox event and waits for an asynchronous projector; pending events force live aggregation | Restore earlier. Same-database point mutations should atomically update source, affected scoped statistics and immutable delta history. This removes routine projection lag and fallback load. |
| Domain ownership | Study mutation methods produce ScreeningDelta or AnnotationDelta because the aggregate knows the exact transition | Domain facts are translated later by a reporting projector | Combine. Keep domain transition classifiers/value objects, but make the persisted delta contract versioned and catalogue-owned so domain behavior is not coupled to a wire DTO. |
| Delta semantics | Signed integer moves, including decrement old tally bucket and increment new bucket; inverse property is mandatory | Generic event replay and immutable replacement blocks | Restore earlier. Targeted signed bucket moves are smaller, naturally idempotent with an operation ledger and directly testable against the legacy query. |
| Transaction boundary | Study plus current statistics in one MongoDB transaction | Source transaction and later projection transaction | Restore earlier. Add the bounded source-operation receipt and immutable uniquely keyed delta/history record to the same source transaction. Source, receipt, current projection and Delta all commit or none do. |
| Driver retry behavior | Transactional atomicity was specified, but callback replay and unknown commit details were incomplete | Stable event IDs and applied-event checks cover redelivery | Combine. Allocate OperationId outside the callback, reread/guard the authoritative revision, generate deterministic deltas, use the all-mode source-operation receipt plus an applicable unique Delta, resolve unknown commits from those records and source revision, and never perform irreversible external effects inside a retryable callback. |
| Source-only idempotency | Earlier operation tracking assumed materialized writes remained available | Kill switch or ledger pressure can commit source without a Delta | Retain newer fallback, add a bounded safety receipt. Every source change commits a minimal unique operation receipt even when projection writes are disabled. Retain it through retry/redelivery/audit floors; reject old or over-capacity operations before changing source rather than risking a duplicate. |
| Physical current model | One current document per project in pmProjectStats | Manifest, immutable blocks, generation partitions, parent chains and CAS publication | Combine, simplified. Keep one logical ProjectStatisticsProjection but store scoped rows by natural family/scope/publication generation plus a small control/summary. A family guard bounds visibility; each scope resolves its newest row at/below that generation, preserving unchanged scopes without payload copies or parent chains. |
| Natural partitions | One project payload with project, stage and membership dictionaries | Hash-partitioned immutable block index | Restore the earlier named domains, revised physically. Use project/stage, reviewer or reviewer-stage, question and search scopes. Natural keys make authorization, repair and delta writes explicit; unit and outcome scopes are excluded. |
| Shared summary contention | Not treated as a blocker; expected reviewer counts are small | No source write may touch any shared project statistic | Reject the newer prohibition. A small project summary is acceptable. Benchmark 1, 2, 5 and 10 concurrent reviewers and retry rates. Striped counters are a measured fallback only. |
| Document-size strategy | Estimated dictionaries and pair matrices fit below MongoDB's 16 MB limit | Assume one project document will eventually become unsafe | Combine. Natural partitions remove most structural risk, but the sparse screeningTallyCounts[(NumberScreened, IncludedCount)] dictionary remains data-dependent. Every physical document has measured dynamic-key and encoded-BSON admission ceilings no greater than 50,000 keys/8 MiB. A new key that would cross either limit is rejected before its MongoDB update; source commits through the source-only receipt/invalidation path, the family becomes Stale/capacity-ineligible and reads fall back authoritatively. Backfill proves exact size before publication. Do not use the old estimate or a family-wide ceiling to justify an unbounded document. |
| Superseded current rows | One document overwritten in place | Token-addressed candidates coexist with current rows | Retain bounded candidates, add reclamation. After a guard flip, keep each scope's newest row at/below VisibleGeneration and active candidate/lease/build references; delete other rows in bounded batches after the oldest snapshot-reader grace boundary. Pressure disables materialized writes rather than growing without bound. |
| Immutable history | Sparse daily copies of current plus a tracking sentinel | Immutable versioned generations and reference-aware retention | Combine. Every accepted mutation has an immutable delta/provenance record; user-facing checkpoints remain sparse, truthful and bounded. No full snapshot per mutation and no mutable sentinel failure array. |
| Current/history atomicity | Current changes immediately; daily history is a later job and can fail independently | A history generation can reference the same atomic publication | Combine. Source, current scoped changes and immutable delta history commit together. Periodic checkpoint creation atomically publishes a fixed-size root over bounded pages/new observations and may reference unchanged immutable observations from the same coherent logical bundle. |
| Checkpoint manifest size | One project snapshot document | Complete logical bundle references | Retain newer semantics, revise physically. Publish a fixed-size root with an ordered-page Merkle root over BSON-bounded reference pages and immutable observations. References use the same deterministic full key as the technical plan—(MetricFamily, MetricKey, ScopeType, canonical ScopeTuple, ObservationId)—so page boundaries and range lookup remain stable when families share a typed scope. Unpublished pages are invisible; the root is the atomic visibility boundary and page proofs keep scoped reads bounded. |
| Checkpoint publication safety | Daily job copies current state | Sparse immutable checkpoint publication | Combine. Root names the exact BuildToken. A materialized build requires a Fresh compatible snapshot with zero active fences and CASes global/project/family guards. An authoritative build may publish during source-only, kill-switch or fenced projection intervals; it CASes its captured source revision and authoritative visibility boundaries and rejects concurrent source/visibility changes without requiring projection Freshness. |
| Checkpoint identity | Daily snapshots were primarily date-addressed | Source and projection revisions can advance independently | Retain newer revision provenance, use both clocks plus the mode epoch. Key a checkpoint by the ordered three-component identity (source revision, projection revision, fleet-wide mode epoch) so source-only changes, projection-only repairs and reviewer-mode transitions can each append without collision; expose an opaque stable cursor/ID over the complete identity. |
| Provenance | Snapshot date, settings snapshot, first/last run and failures | Schema/source/catalogue versions, source watermark, reason and operation IDs | Retain newer. These fields are required for interpretability, retry dedupe and safe schema upgrades. |
| Retention | Changed daily observations indefinitely, fill-forward in UI | 90-day daily, weekly to one year, monthly to five years, maximum 256 | Retain newer provisionally. Bounded storage is mandatory; Phase 0 must capacity-test and approve the exact numbers. Compaction selects real observations only. |
| Delta-ledger retention | Operation tracking was not bounded independently | Immutable operation history and rebuild replay | Combine with a hard bound. Keep exact identities and replayable moves only above measured floors; compact safe ranges into at most 64 mergeable seals. Below-floor redelivery fails closed. Stop materialized writes if safety prevents compaction before the ceiling. |
| Bootstrap | Legacy to Backfilling to Live with a short review-write freeze | Snapshot/replay generation build while ordinary writes continue | Combine. Use scoped fencing and authoritative fallback; capture a source boundary and replay committed deltas. Permit a short project-scoped write gate only if the repository cannot prove a replay boundary and a measured operational limit is approved. |
| Authoritative aggregation role | Initial baseline, three-way audit and repair | Routine fallback while every point event is pending | Restore earlier. Full aggregation is for backfill, scoped recovery, formula/schema upgrades and periodic reconciliation, plus exceptional fallback—not every normal write. |
| Rebuild concurrency | Read-only snapshot transaction and later correction write | Lease, watermark, replay and publication fencing | Retain newer. Scoped leases, source boundary and stale/rebuilding states close races the earlier correction write did not fully address. |
| Repair policy | Three-way audit could overwrite current from a selected source | Repair creates a new generation and evidence | Combine. Compare materialized state, legacy facets and independent streamed classifier at one provable boundary. Never blind-overwrite; fence, record mismatch evidence and publish a scoped rebuild with provenance. |
| Ordinary fallback | Legacy or live state selects one read model | Whole requested bundle falls back if any block is stale/pending | Combine. Keep coherent requested-bundle fallback, but a successful transactional point mutation has no pending interval. Missing, stale, rebuilding, incompatible, killed or fenced scopes still use authoritative reads. |
| Read consistency | State-aware reads, but no complete control/scope snapshot predicate | Control plus naturally partitioned scope reads | Retain newer fallback, strengthen the read. Require one MongoDB snapshot-read transaction over global/project control, scopes and fences. Separate majority reads are prohibited because an older scope can look unchanged; snapshot failure falls back authoritatively. |
| Bulk import | Insert with PendingImportJobId invisibly, calculate bulk delta, then atomically reveal studies and update stats | Generic fence, batched mutation, authoritative rebuild and replay | Combine. Retain PendingImportJobId, but make visibility one bounded Project.ActiveHiddenImportJobId gate read by authoritative queries. A separate ActiveImportOperationId blocks overlap until every family publishes; per-Study cleanup never reveals partial batches. |
| Bulk deletion | Mark PendingDeletion, then classify and delete batches with matching negative deltas | Generic fence and rebuild | Combine. Preserve per-batch delete plus negative delta transactions and restartable two-phase progress. Keep the affected scope fenced until every batch and final reconciliation prove completeness. |
| Bulk final publication | Final update assumed bounded enough for one transaction | Publish rebuilt scopes and clear all fences atomically | Combine, keep every transaction bounded. Stage token-addressed candidate scopes by stable batch ID behind a per-family marker/guard, catch up ordered deltas, clear fences in batches while the guard forces fallback, then flip one constant-size guard transaction with a new public revision and replacement-provenance record. |
| Search add/remove | Search population changes included in import/delete mechanics | Search is only a generic population invalidation | Restore earlier specificity. Search scope is a natural partition and every systematic-search create/import/rollback/delete path must own exact population deltas or a fenced rebuild. |
| Threshold/settings changes | Recompute derived counters from settings-independent tally dictionaries without Study queries | Always fence and rebuild | Restore earlier when bounded. Reclassify from raw tallies transactionally. Use fenced authoritative rebuild only when the formula needs source facts not present in approved tallies or exceeds measured bounds. |
| Membership changes | Active-membership totals and reviewer dictionaries adjusted; deactivated historical facts retained | Generic family fence plus authorization reevaluation | Combine. Apply bounded membership-scope deltas/invalidation synchronously; retain immutable historical facts, always apply current authorization, and rebuild only when membership changes alter a non-additive denominator. |
| Annotation question changes | Question deletion resets affected completed sessions to incomplete and requires bulk annotation delta | Version-aware question family with generic fence | Combine. Retain the exact reset transition and negative/positive bucket moves; add immutable question definition/version identity, scoped fence and rebuild for unbounded schema/content changes. |
| Slot reservations | Predates durable active-reviewer reservation state | Current plan treats durable reservation transitions as statistical inputs | Retain newer conditionally. Catalogue only the current authoritative availability/capacity formulas affected by durable reservation changes. Live connection presence remains operational, not statistics. |
| Annotation unit scope | Older plans sometimes implied reusable unit dimensions | Explicitly excluded | Retain newer. A unit is per Study/reviewer annotation; no cross-study aggregate or measured need exists. Re-entry requires a separate approved feature. |
| Outcome scope | Older broad plan and partition vocabulary allowed question/outcome scope | User decision removes outcome materialization | Reject earlier. Outcome is not a shared typed scope or metric family in this programme. Like unit scope, it may return only through a separately approved feature with a concrete authoritative aggregate and consumer. |
| Kappa/agreement | Project Fleiss from screening tallies and pairwise Cohen matrices, including cross-stage assumptions in the stale implementation | All agreement/kappa excluded | Reject earlier implementation. Do not revive cross-stage pairwise calculations or broad kappa UI. Existing raw decision-profile tallies remain; any derived agreement statistic needs a separately approved same-stage cohort and denominator. |
| Query shape | FullStats-compatible current document, history endpoint, REST/SignalR permission filtering | Domain-neutral current/history metric-key queries and typed unavailability | Combine. Retain current wire compatibility through adapters, but expose one authorized catalogue query contract for current and history with repeatable metric + canonical project/stage/membership/membership-stage/stage-question-version/search scope pairs. Heterogeneous bundles retain one-snapshot fallback without an ambiguous cross-product. A transport/streaming raw-body and raw-item cap applies before parsing/canonicalization (duplicates count), followed by hard deduplicated selection-count/encoded-key limits before a snapshot, so repeated input cannot consume unbounded parser work and pages cannot invent stores or fetch unbounded families; exact missing history returns typed unavailability/410. |
| Authorization | Explicit project/stage graph and leaderboard permissions; own reviewer row remains visible where currently allowed | Current authorization reapplied to stored superset | Combine. Preserve every earlier permission rule and own/peer distinction, apply current authorization on every current/history request, and never serialize persistence documents directly. |
| SignalR | Change stream pushes the whole current stats document with per-user filtering | Publish client-invalidation revision/family invalidation and refetch authorized data | Retain newer, make durable. Insert the invalidation into a dedicated MongoDB outbox in the source/receipt/current/delta or projection-only publication transaction. A leased at-least-once dispatcher publishes after commit; clients deduplicate by family/client revision and refetch. Retryable callbacks never push externally. |
| DTO read path | Remove sync-over-async aggregation from Project DTO mapping and use one current lookup | Defer all page cutover until shared projection phases | Combine. Remove hot aggregation only consumer-by-consumer after parity. Preserve compatible DTO adapters during migration and independently flag each cutover. |
| Parity source | Legacy query, stored stats and independent streamed classification | Generic shadow parity | Restore earlier specificity. Keep the three-source model at a single provable source boundary, with separate two-source checks only for metrics lacking a proven legacy facet. |
| Parity fixtures | Real production patterns, local Mongo backup, empty/new/rescreen/overscreen/annotation lifecycle, inverse delta | Representative corpus without named legacy cases | Combine. Make the recovered cases mandatory and add mixed reviewer decisions, current 15-facet fixtures, retry/redelivery, membership/question/search mutations and authorization views. |
| Query-level tests | Classifier results compared directly with legacy MongoDB aggregate | Mostly domain/repository/API parity | Restore earlier. A query-level fixture must execute the real authoritative pipeline and compare every catalogue bucket, not only compare two C# classifiers. |
| Performance proof | 1,000/10,000/50,000-study estimates and Atlas transaction limits | 20% read-p95, 80% aggregation reduction, under 10% mutation regression | Combine. Keep acceptance thresholds, add fixed datasets and realistic reviewer concurrency. Measure transaction p50/p95, retries, write conflicts, oplog/60-second limits, per-scope bytes, facet count and aggregation frequency. |
| Rollout state | Legacy, Backfilling, Live; new projects go directly Live | Global/family/consumer flags, allowlist, shadow mode and Fresh/Stale/Rebuilding/Incompatible | Combine. Use newer flags and lifecycle. Do not label a project Live until each requested family passes; independently roll back consumers. |
| Distributed write disable | No rolling-deployment flag barrier | Cached runtime write kill switch | Revise newer. Use a durable monotonic WriteEpoch and Enabled/Disabling/Disabled control read inside every source transaction and serving snapshot. Cached flags only request transitions; re-enable requires compatible writers and epoch-matched rebuilt scopes. |
| External side effects | Change stream and jobs were described after database updates but callback constraints were implicit | Event/outbox architecture separates publication | Retain the safety rule, make invalidation durable. Email, broker publish, SignalR, object storage and external APIs are forbidden inside retryable MongoDB callbacks. Required revision/scope invalidations use a dedicated durable transactional outbox; it is not the routine statistics update path. |
| Project deletion | Delete current and historical statistics | Tombstone, deny, then policy-driven purge | Combine. Deny immediately and purge under the Project retention policy; the projection creates no independent retention right. |
| Frontend history | Daily sparse snapshots, fill-forward, failure markers and area-chart wiring | Backend history first; visible UI optional | Retain newer phasing, preserve earlier contract evidence. Durable API history is required; UI activation is a later independently reviewed consumer using real checkpoints and explicit gaps. |
| PR #2985 | Did not exist | Screening-specific current/history candidate | Retain as evidence, revise before reuse. Reuse classifiers, lifecycle, flags, lease, telemetry and tests where they fit scoped transactional documents; supersede screening-only schemas and per-mutation full-payload snapshots. |
Preferred physical model¶
One logical ProjectStatisticsProjection is stored in naturally partitioned documents:
| Document | Natural key | Role |
|---|---|---|
| ProjectStatisticsGlobalControl | singleton | Durable Enabled/Disabling/Disabled mode, monotonic WriteEpoch and GlobalClientInvalidationRevision checked inside every source transaction and materialized-read snapshot |
| ProjectStatisticsControl | project | Small versions, flags, lifecycle summaries, scalar source/projection/client-invalidation clocks, delta replay/identity and source-operation idempotency floors, last reconciled revision and deletion state |
| ProjectStatisticsSummary | project + bounded family + publication generation/token | Small approved project-wide totals/profile buckets selected like other scopes; splitting/striping still requires measurement |
| ProjectStatisticsScope | project + family + typed scope key + schema version + publication generation/token | Mutable visible/candidate counters and deletion tombstones; each scope first selects the exact visible generation/token row when present or otherwise the greatest older published row, then checks compatibility; an incompatible newest selected row rejects the materialized bundle rather than falling through, while unchanged older rows remain visible without exposing abandoned candidates |
| ProjectStatisticsSourceOperationReceipt | project + stable namespaced operation ID | Minimal bounded all-mode idempotency receipt for source changes, including source-only fallback; retained through retry/redelivery/audit floors and capacity-fail-closed |
| ProjectStatisticsRevisionRecord | project + unique namespaced record ID; ordered index and cursor by project + committed revision + stable RecordId | Delta uses stable operation/batch identity and carries replayable signed moves; all bounded sibling records at an equal revision must be consumed before that revision is complete; each bounded ReplacementPublication uses deterministic operation + family PublicationId, references a paged manifest and forms a replay barrier; PublicationAbort/SourceImportAbort preserve failure provenance without claiming publication; uniqueness spans upgrades |
| ProjectStatisticsRevisionRangeSeal | project + compacted revision range | Bounded mergeable audit digest below the delta replay/identity floors; never used to replay an individual operation |
| ProjectStatisticsCheckpointSet | project + three-component checkpoint identity (source revision, projection revision, fleet-wide mode epoch) | Fixed-size immutable publication root with selected BuildToken, page/reference counts and Merkle root for one complete logical checkpoint; source-only changes and projection-only repairs both append without collision |
| ProjectStatisticsCheckpointReferencePage | project + build token + ordinal | Immutable BSON-bounded, typed-scope-key-ordered page with content digest, key range and Merkle proof, allowing authenticated scoped reads; unpublished pages are invisible without the root |
| ProjectStatisticsCheckpointObservation | project + metric/family/typed scope + first-observed three-component checkpoint identity (source revision, projection revision, mode epoch) + content digest | Immutable real scope occurrence; an unchanged observation may be referenced by later checkpoint sets, while A→B→A creates a distinct later A occurrence rather than reusing pre-B provenance |
| ProjectStatisticsCheckpointBuild | project + BuildToken, with one active marker per CheckpointId | Bounded durable Building/Published/Abandoned/Cleaning/Cleaned marker, monotonic renewable lease, captured boundaries, counts/digest and restartable orphan-cleanup cursor; root publication CASes the marker and expired owners cannot publish or delete |
| ProjectStatisticsCheckpointBuildAdmission | project | Singleton admission and byte/page reservation for one unpublished build token across all CheckpointIds; newer requests coalesce without allocating artifacts, and an expired/abandoned build must be cleaned before successor admission |
| ProjectStatisticsGlobalNotificationOutbox | singleton + notification kind | One bounded coalescing global slot updated atomically with GlobalClientInvalidationRevision and broadcast to every statistics subscription without enumerating projects |
| ProjectStatisticsNotificationOutbox | project + metric family + notification kind | One bounded coalescing slot with monotonic target/delivered ClientInvalidationRevision and leased at-least-once delivery; source and projection-only changes both invalidate, and a reserved project-wide visibility slot reaches every family subscription |
| ProjectStatisticsOperationFence | project + operation + family/scope | Restartable bulk/import/delete/configuration boundary and progress |
| ProjectStatisticsRebuildLease | project + family/scope | Fenced lease for authoritative rebuild and publication |
| ProjectStatisticsPublicationGuard | project + bounded metric family | Independent monotonic GuardVersion plus constant-size VisibleGeneration/VisibleToken, one candidate generation/token, lifecycle and active-fence count used by serving/checkpoint CAS |
| ProjectStatisticsPublicationOperation | project + operation | Bounded operation-wide coordinator with fixed affected-family set, captured project/guard revisions and Preparing/SourceComplete/AllFamiliesAdmitted/Completed/Aborting/Aborted lifecycle (forward batches only in Preparing; the SourceComplete seal revokes the loader generation and is the state from which admission proceeds); the same owner may CAS AllFamiliesAdmitted back to SourceComplete, never to Preparing, after a lost final-publication race so it can catch up and re-admit; recovery authority across the admission-to-final-commit crash boundary, with measured per-project row/byte admission ceilings |
| ProjectStatisticsPublicationMarker | project + operation + family | Bounded idempotent ready/fence/page counts, manifest reference/digest and candidate token for constant-size final bulk publication |
| ProjectStatisticsPublicationManifestPage | project + operation + family + build token + ordinal | Immutable bounded affected-scope/tombstone manifest page, never embedded in the final revision transaction |
| ProjectStatisticsCurrentCleanup | project + family + superseded generation | Restartable reachability cursor/grace watermark for bounded deletion of non-selected old current rows |
| ProjectStatisticsPublicationManifestCleanup | project + family + manifest | Restartable floor/reference-aware reclamation of published, abandoned and aborted manifest pages |
| ProjectStatisticsPublicationOperationCleanup | project + operation | Restartable floor/reference-aware reclamation of terminal coordinators; non-terminal coordinators are never TTL-deleted |
| ProjectStatisticsOperationFenceCleanup | project + family + operation | Restartable floor/reference/snapshot-safe reclamation of terminal fence rows; active fences are never TTL-deleted |
| ProjectStatisticsSourceImportRollbackCleanup | project + import operation | Restartable floor/reference/source-postcondition/snapshot-safe reclamation of terminal identity/before-image rollback manifests; active or retryable manifests are never TTL-deleted |
The authoritative Project additionally carries at most one ActiveImportOperationId and matching ActiveHiddenImportJobId. Imported Studies keep PendingImportJobId through bounded loading. Authoritative queries exclude exactly the hidden token in one source snapshot. Every affected-family manifest must pass bounded admission first; one bounded all-family transaction flips every guard, clears both Project gates, makes every imported Study visible together and advances all family plus project-wide notifications. There is no partial-family publication, and later per-Study cleanup has no visibility semantics. A terminal source-loading error instead uses a paged rollback manifest to delete the still-hidden prefix and atomically releases both gates only after zero matching rows and candidate artifacts are proven; it never reveals a partial import or leaves the next import blocked. Successful and aborted rollback manifests are reclaimed only after terminal operation, idempotency, audit, redelivery, reconciliation, reference and snapshot-reader floors prove they cannot be needed again.
An ordinary supported mutation performs one transaction:
- load or guard the source aggregate and source revision;
- calculate deterministic before/after classifications and signed bucket moves;
- insert the bounded all-mode source-operation receipt or prove its identical retry;
- write the authoritative source mutation;
- apply targeted increments/decrements to every affected natural scope;
- insert an immutable uniquely keyed
(project, revision, stable RecordId)-orderedDeltarevision record and advance the project committed-revision clock; and - advance source/client invalidation revisions and enqueue required family invalidations in the durable transactional notification outbox; dispatch occurs only after confirmed commit.
A duplicate operation with the same source revisions, schema/source versions and digest is a successful no-op. The same operation ID with different revisions, versions or digest is a conflict that marks affected scopes stale and requires investigation. MongoDB callback retries must never generate new IDs or repeat irreversible external side effects.
Exact delta identities are retained only above measured replay floors. The separate minimal source-operation receipt remains the source-mutation idempotency authority in materialized and source-only modes; receipts compact only after the redelivery/audit floor, and older operations fail closed. Safe contiguous delta ranges compact into at most 64 mergeable range seals. If delta compaction cannot keep within its configured ceiling, materialized writes stop and authoritative fallback takes over; if the bounded receipt cannot be admitted, the authoritative source mutation itself fails before changing data.
Recovered validation contract¶
Phase 0 must turn the following into named fixtures and commands:
- query-level equality against all 15 current facets;
- the consumer-visible
FullStatsidentity envelope, not only its metric blocks:MembershipIds(every membership for an administrator request; only the selected investigator's membership for a restricted request) and the derivedStageIds, with an administrator-view and a restricted-view fixture for both lists so a replacement adapter cannot pass every metric fixture while omitting or mis-scoping them; - zero studies; one screening; exactly-threshold; below/above agreement threshold; Inclusion exactly 0.5; valid mixed include/exclude reviewer decisions; rescreening; and more than three screenings;
- candidate and reconciliation session start, incomplete, completion, deletion and question-reset transitions for excluded and unexcluded populations;
- membership-stage mapper parity with unequal all/sessioned/investigator values for candidate availability, unavailability, completion and every reconciliation output under both AllowSelfReconciliation states;
- outer merged membership-annotation parity with non-zero unequal excluded values under each ExcludedSessionStatsGrouping mode, including the Separate excluded block;
- membership own/peer views, inactive membership history, project/stage permission changes and project-wide invalidation of every affected family subscription;
- unequal project/investigator sufficiency and overscreening values proving membership output preserves
the current project-wide mappings, plus unequal investigator
SufficientlyIncludedAndAgree/SufficientlyExcludedAndAgreeinputs proving the mapper duplicates the excluded-agree value into both output fields, and unequal project/investigator sufficiently screened and insufficiently screened values proving the exactAvailable/Unavailablesubtraction formulas; - search import, spreadsheet import with screenings, rollback, deletion and redelivery;
- delta inverse property and old-bucket decrement/new-bucket increment assertions;
- callback replay, transient transaction failure, unknown commit, duplicate operation and conflicting duplicate;
- concurrent screening of the same Study and different Studies at 1, 2, 5 and 10 reviewers;
- three-source parity at one snapshot boundary: materialized projection, current StudyStats facets and independent streamed classifier;
- sparse checkpoint ordering, truthful gaps, retention selection and rebuild provenance; and
- source-only or compacted scope-proof gaps forcing a new real observation even when the digest repeats;
- cold/warm 1,000, 10,000 and 50,000-study reads plus mutation latency, retry/conflict rates, oplog size, transaction duration and storage growth.
Decisions still requiring Phase 0 evidence¶
- Whether Inclusion equal to 0.5, the strict agreement comparison, overscreening above 3 and the annotation minimum of 2 are intended product semantics or legacy defects. Materialization mirrors them until a separate correction is approved.
- Which current import consumer and repository methods own staged visibility after the later import redesign; the old SearchImportJobConsumer name is not authoritative.
- Which question-answer transitions are safely incremental and which schema/suppression changes require a fenced rebuild.
- The maximum bounded setting-change recomputation and import reveal transaction before the design switches to fenced batching/rebuild.
- The measured contention threshold that would justify splitting the small project summary or using striped counters.
- The exact checkpoint retention/capacity values and whether a short bootstrap write gate is ever operationally preferable to boundary capture plus delta replay.
These are evidence gates, not reasons to revert ordinary point mutations to asynchronous projection.