Programme integration and strategic changes¶
Temporary planning document; planning only. This page authorises no code, flag change,
migration, deployment, GitHub write or message to anyone. Existing programmes keep ownership of
their code and PRs (OPS1, OWNER; integrated plan §7
rule 4). Everything here that is not tied to an owner decision ID is PROPOSAL. Questions cite the
Batch D IDs in open questions; this page mints none.
1. Purpose, status and refresh time¶
Chris asked on 3 October that "study allocation, study pool partitioning, active reviewer tracking, materialised stats and notification service which are at various degrees of completeness are also reviewed, integration with plan taken into account and any changes to those implementations are strategically suggested and thought through". This page does that. For each programme it gives: the current state with evidence; how the plan integrates with it (touch points, conflicts, double ownership, missing contracts, unstated assumptions); the changes recommended to the existing implementation, each with rationale, timing against the plan's releases and freeze gates, compatibility, risk, PR slicing and owner; what should not be built further yet; and what the plan adopts from the programme. It also covers progressive batches, the paused eligibility programme, authorization, the architecture review (#3961) and the smaller prerequisites, and it replaces the plan's external-joins table (integrated plan §5.11).
It resolves every finding in the round-2 programme reviews AP, RT, MS and NS, plus DS-02 and DS-04, PH-02, PH-04, PH-09, PH-14, PH-15 and PH-20 and DC-05 and DC-21. The per-finding outcome is in the resolution record.
Companion documents own the mechanics and are linked, not repeated:
consistency model (transactions, Study.CanonicalSummary, the command
ledger, ownership markers, durable effects, the FEAT-024 source-write seam mechanics),
versioning model, domain model (aggregates, claim value
object, enrolment record) and delivery operating model (gates,
critical path, F1a/F1b/F1c). Where this page says "F1a", it means the engine-contract freeze of that
split; before the split lands, read it as F1.
Precedence: the owner ledger, then decision register §1.11 to §1.13, then the round-2 adjudications, then this page's proposals.
Decision recorded after the brief: Chris decided D1-01 on 3 October: keep #3964, port the
active-member check and its tests from #3969 into it, and close #3969 (OWNER; the ledger owner
records it in register §1.11). It has been carried out: #3964 merged at 20:27 BST (merge commit
85e6facf7) and #3969 is closed (refresh row "Re-check 20:35 BST" below). Later that evening Chris
approved D1-02 to D1-09 as recommended
(register §1.13), so no D1
question is open. Still to come: a date for activating #3987's families (D1-03) and the stack
owner's agreement to restack #3965 onto #3944 (D1-09, A-33). The live-state refresh rows below were
not re-read for this update.
Refresh (live state used throughout):
| Source | Read at | State |
|---|---|---|
GitHub PRs and issues (gh pr view, gh issue view, gh pr list) |
14:18–14:30 UTC (15:18–15:30 BST), 3 October | Per section |
main |
14:18 UTC | de3e98c59. Since the reviewers' 0f5c61073: #3967 (invitation token matching, 886089c5d), #3971 (production promotion blocked on main integration tests), #3970, #3968. None touches allocation, tracking, eligibility, batches or statistics code; #3967 changes invitation-token matching, which #3938's invitation capture also touches (conflict not checked) |
| cluster-gitops | 14:20 UTC | The local checkout 7c377aa4 (2 October) is 55 commits behind its remote-tracking origin/main ea972fd6 (13:37 UTC, 3 October). Environment values here come from ea972fd6 (read with git diff, no fetch). The round-2 reviewers read 7c377aa4. One material difference: staging now pins materializedProjectStatisticsFold: true on API and PM, with SYRF__ProjectStatistics__Fold__AllowPartialCoverage on the API (§7.1) |
| Local worktrees (read-only) | 15:20 BST | #3964: three commits not yet pushed (ce83b232a, 8daad48ca, 1688b4f94). #3965: four commits not yet pushed (4f19b5672…b30ba5d2e) plus uncommitted edits in progress. #3969: one pushed WIP commit. Stack top ae3c6749d |
| Re-check | 19:25 BST | main eb93caffa (one commit since de3e98c59: auth-migration E2E live rows, #3994; outside every programme here). #3965 now has eight commits not yet pushed (local head 8432e3aeb; the fix round adds all-or-nothing thread creation, a content-free "questioned" marker for every reconciler, stable reviewer labels and typed reply refusals; the reconciler self-review check is still same-stage only). #3964 unchanged (three local commits). #3969 still open. #3961 updated at 18:13 UTC (production-approval controls; a new frontend bug-fix plan, §10). Every other PR state above unchanged |
| Re-check | 20:35 BST | #3964 merged at 20:27 BST (19:27 UTC), merge commit 85e6facf7, after an approving Claude review on head 16788f9 and green checks; its worktree and branches were removed. #3969 closed (19:27 UTC) with a comment pointing to #3964 (D1-01 carried out). #3965 open and ready: ten commits pushed, head 986b1cdc2, base codex/checked-pdf-proposals-and-explicit-administrator-a-r6bdl2 (#3947's branch); the tenth commit refuses a reconciler who reviewed the study in any stage (gh pr view, gh api). It waits for the stack (D1-09). Present-tense statements about #3964, #3965 and #3969 on this page use this row |
Limits: the drafter of this page made no database reads and no runtime-override reads, and ran no tests or benchmarks. The orchestrating session made two read-only queries on 3 October: a count of stored owner-reserved grants (ChangeOwner, AssignPermissions, Delete) in production and staging, which found none; and a count of legacy reconciled sessions in production, which timed out on an unindexed scan and was not retried (to run off-peak before F4). Claims that could not be checked are marked UNVERIFIED where they appear and listed in the final report.
2. Summary¶
2.1 Summary table¶
| Programme | State (15:18 BST; #3964, #3965 and #3969 at 20:35 BST) | Joins (external joins and gates) | Top recommended changes | Batch D |
|---|---|---|---|---|
| Proportional allocation and pool partitioning (FEAT-025) | Merged, dark. Flag set only in preview pr-3327. Reviewer validity (Chris, 22 September) not met: blocked on #3251. STATUS stale. Never accepted by a human (#3269, #3327) |
X-AUTH-RESOLVER (#3251) → R3a per-reviewer preview, AL1; F-A; AL1 placed after allocation Phase 2 | Membership-facts seam (E64); refuse allocation on canonical stages until AL1 (E65); RA5 scoped admission (E66); one reservation migration (E68) | D3-13a, D3-13c, D3-13d |
| Progressive shared batches (#3936, #3939) | Plan open and mergeable; implementation open, conflicting, 63 files; nothing on main |
X-BATCH (rewritten) ← X-ELIG; X-BATCH → R3c | Slice #3939; evidence seam; pool-entry-based membership; durable opening that emits pool-entry events; read-only status; performance gate (E67) | D3-13b, D3-13e, D3-13f |
| Review eligibility (paused) | S1a–S4-A and the claim-revoked event merged, dark; #3746 conflicting; #3742 empty draft; #3741 draft. Flag reaches the API host only | X-ELIG enumerated (S4-B, S4-C, S6a, S6b, fixed-two correction, Project-token redesign, flag delivery to PM) → R3a production and X-BATCH | R3a absorbs S6b; D8 mapping; StageSettings version replaces the Project token; flags to PM with an agreement check (E75) | D3-09 |
| Active reviewer tracking, claims and presence | Merged; off in every deployed environment; on in the E2E stack; reconciliation excluded; M15 transition has no caller; #3876 deferred | X-RECLAIM → R4a in every environment; X-CLAIMS → R2b and R3a production claims; claim contract v2 at F1a | Claim contract v2 and one migration (E68); production claims route (E69); R0 and R2a adapters (E70); E2E in both modes; orphan backstop | D2-07, D2-08, D3-16, D3-17, D3-18, D3-19, D3-20 |
| Materialised statistics (FEAT-024) | Slices 0–7 merged, dark; gate (b) provisional fail; soak open; staging fold flag now pinned on; production refused in code; no FEAT-024 PR open | X-STATS-a → R2c staging pilot; X-STATS-b1…b7 → R2c production; X-STATS-c (target-aware classification) → R2b pilots on allowlisted projects, R3a | Source-write seam (R1); target-aware classification at all three fixed-two sites (E71); canonical-sources amendment (E72); protocol 5 once, after gate (b); #3524 after C16 | D1-03 (decided), D1-08 (decided), D3-10a–d, D3-11 |
| Notification service | Eight stacked PRs plus #3965, none merged; #3932 conflicting; stack E2E never run in CI; #3965's Q-10 code pushed and ready for review, stacked on #3947 | X-NOTIF (defined as stack steps 1–5 merged) → notices in R1c, R2c, R3c, R4a, R4b; G-NOTIF per environment and kind family | Tolerant preferences before #3942 merges; C15 v2 (E73); enablement controls (E74); merge order with #3965 restacked | D1-09 (decided), D3-01, D3-07, D3-21–D3-25 |
| Authorization (#3335) | Active; M5b merged; WP1d, WP9, WP11, WP-M1/M2 open; ownership fix #3964 merged (D1-01) | X-AUTH-SCHEMA, X-AUTH-ENFORCE, X-AUTH-WP9 → R1b/R1c; X-AUTH-RESOLVER → R3a, AL1 | Out-of-request resolver on the single evaluator; presence, study-issue and PDF-correction capabilities in the C10 catalogue | D1-01 (decided) |
| Architecture review (#3961) | Draft docs PR; issues #3972–#3990 open; Phase 0 fixes #3967, #3968, #3970, #3971 merged | X-ARCH-a (#3985, #3973) → F1a; X-ARCH-b (#3986) → R0; X-ARCH-c (#3975) → R0 and G-NOTIF; X-ARCH-d (#3979, #3980) → R3a | One joint roadmap; one writer per collection for canonical collections | D1-02 (decided), D1-03 (decided) |
| Other prerequisites | Deletion lifecycle (ADR-014 uncommitted), search import timeouts, flag overhaul P7, job classification, AF2 admission slices, AF2 pdf-tools, FEAT-023 |
X-DEL, X-IMPORT, X-PDFTOOLS, X-AF2, X-SHELL | Per §11 | D1-07 (decided), D3-01, D3-12 |
2.2 Cross-programme integration principles¶
These five rules are what make the per-programme changes below coherent rather than piecemeal.
All are PROPOSAL.
- One membership-facts seam answers "who holds what on this study". Eligibility pools, the
allocation saved-work exemption and slot rule, typed admission, the capacity guards, batch
completion and FEAT-024 classification all read this reviewer's facts from embedded
Studyfields today. Once canonical sessions, claims and decisions live outside Study, every one of them would answer wrongly.IReviewMembershipFacts(E64) is extracted now with an embedded-Study provider and no behaviour change; theStudy.CanonicalSummaryprovider follows at R0/R2a (shape in the consistency model). The eligibility truth table becomes its conformance suite. - One claim contract. Capacity claims (form slot, profile slot, requested review) and editor exclusion (task, query) share one typed contract (E68), used by tracking, eligibility, allocation and reconciliation, with one reservation migration.
- One per-project admission record. R0's admission service (the enrolment record, named
CanonicalEnrolmentin the domain model) is the single per-project mechanism for canonical scopes, per-project notification admission, tracking pilots, AF2, shell and eligibility per-project admission, and the flag overhaul's P7 targeting. FEAT-024's durable eligibility (#3524) shares its record shape and audit but stays separate, because it must take part in source admission inside the transaction (MS-24). - One durable-effects pattern. Batch openings, claim revocations, notification fan-out, publication phase 2 and adoption record a durable intent in the source transaction and a leased idempotent worker expands it (C19 class b in the consistency model). No frontier or state is recomputed lazily on a read; no in-memory outbox.
- One fence primitive, owned by the engine. Publication drain, stage completion and adoption cutover use the engine's scoped fence. FEAT-024's definition-rewrite fence stays a statistics read fence, never a reviewer pause (MS-04).
A sixth rule follows from DS-04 and Q-25: production enablement is per project, not per environment. Each programme's pilot activation reads the same admission record, so a pilot can start, stop and roll back without a fleet-wide flag change (D1-07).
3. Proportional allocation and pool partitioning¶
FEAT-025, owned by the allocation programme (docs/features/proportional-study-allocation/).
3.1 Current state¶
| Component | State | Evidence | Gaps |
|---|---|---|---|
| Allocation MVP | Merged, dark (CODE-MAIN). #2991 (2 September), #3211, #3212, #3213, #3228, #3267, #3329, #3604, #3611; regime identity and provenance #3603 merged 24 September; tie-break performance 7f8b9f4c1 on 3 October |
gh pr view (all MERGED); git log |
Admin progress budget (warm p95 ≤ 2,000 ms at 100k studies / 50 reviewers) not met; memoisation (STATUS item 8) not started; regime-evolution Phases 2–5 unstarted (regime-evolution-plan.md, Draft) |
Flag proportionalStudyAllocation |
Default false. Delivered to the API host only (src/charts/syrf-common/env-mapping.yaml, block at :1150 with services: [api], entry :1254). Set only in preview pr-3327 (API, PM and web values); unset in staging and production |
cluster-gitops ea972fd6 |
PM never receives the value through Helm |
| Reviewer validity (Chris, 22 September) | Not met. Only active membership is checked at save; the effective-permission check is blocked on an out-of-request resolver | STATUS.md:14-23; #3251 OPEN (updated 22 September) |
Blocks AL1 and the plan's "Who is offered what" |
| Human acceptance | None. #3327 (preview flag vehicle, no code) OPEN, CONFLICTING, last updated 8 September; preview checklist all pending; #3269 OPEN | gh pr view 3327; gh issue view 3269 |
AL1 would build on an unaccepted base |
| Status document | Stale: STATUS.md:71 still says #3603 is "pending review/merge"; last updated 24 September; #3048 is closed (unmerged), not paused |
STATUS.md:7, 71; gh |
Refresh before G0 |
| Pool partitioning | Merged, inert with the flag off. StudyWorkloadShareBucket.FromStudyId (FNV-1a mod 10,000) persisted as Study.WorkloadShareBucket with index ProjectId_1_WorkloadShareBucket_1; the plan walks 10,000 buckets per request |
Study.cs:536-553; StageWorkloadSharePlan.cs:39-57 (per AP) |
Plan memoisation not started |
| Pools and selection | Project-wide predicates minus excluded and own-session filters; random selection is $sample after $match; no StudyLifecycleStatus; FEAT-008's rand field not built |
ReviewEligibilityPoolFilters.cs:63-115; StudyRepository.cs:763, 801 (per AP); docs/features/stage-filtering/README.md:199-237 |
No selection budget |
| Targets | Stage.SessionCountTarget falls back to the project screening threshold; enabling allocation materialises the inherited value and locks target and mode edits |
Stage.cs:177, 360-377 (per AP) |
Screening threshold doubles as annotation target; no decoupling in the plan's adoption mapping |
| Legacy partitions | Unfinished PartitionSet model and an unflagged study-partitions admin route |
PartitionSet.cs; project-admin.routes.ts:53-55 (per AP) |
FEAT-026 warns not to expose them as batches |
| Open issues | #3251, #3252, #3264, #3269, #3321, #3745 (Phase 3: leftover claims under reallocation), #2042 | gh issue view (all OPEN) |
— |
3.2 Integration with the plan¶
Per-reviewer membership projection (AP-01, Corrected). Every allocation and admission read
takes this reviewer's membership from embedded data: own ordinary session and status, own claim
per activity, own screening decision, and which other reviewers already hold work (for the D8 slot
rule) (ReviewEligibilityPoolFilters.cs:77-84, 117-123, 140-150;
StageWorkloadShareEligibility.cs:73-84; AllocationClaimSlot.cs:29-39;
ActivityReservationAdmission.cs:41-58; StageReviewService.cs:539-541, 676-681; per AP; the
orchestrator re-checked the pool-filter reads). E20's "bounded per-bound-stage tallies and flags" would therefore
re-offer completed studies, deny an out-of-bucket resume with 404 and undercount slots for every
canonical session. Correction: E20 is a per-form, per-reviewer membership projection (per form,
one member per reviewer: {state: placeHeld, savedIncomplete, completed or withdrawn; standing:
qualifying, needsUpdating, pinnedOlderCounted, pinnedOlderNotCounted or notApplicable; versionSeq;
claimActivities; admittingRegimeId; routeStageId}; per profile the reviewer's current decision;
derived tallies), carried by Study.CanonicalSummary (brief §1.3; shape and write rules in the
consistency model §3.3). draft_only is never
written to Study: readers take it from pmFormSession/pmSessionDraft, and it appears on Study only
as placeHeld when a Study-writing command recorded it. It is bounded
by the SF4 rule (every qualifying candidate), not by the target. The programme-side change is the
seam E64. A parity fixture (every truth-table row answers identically from the projection and from
embedded data) becomes part of AC-M0-04's C7 identity sign-off.
Allocation on canonical stages (AP-06, D3-13a). The regime evaluator reads
stage.SessionCountTarget and ConfigureWorkloadShares requires ReviewMode.Annotation and
StudySelectionMode.Annotation (Stage.cs:53-68, 150-191; StageAllocationRegimeEvaluator.cs:97-104,
per AP). A canonical stage has neither: the target is form-owned (SF2), the mode is replaced by
StageSettings, and the #3732 override is legacy-only. A form-version publication could change the
target under a published regime. Recommended (D3-13a): refuse allocation on every canonical stage
until AL1: ConfigureWorkloadShares refuses a canonical stage, and R0 refuses to admit a stage
whose allocation is enabled unless AL1 is live. A-09 is widened to cover single-stage canonical
forms (open questions A-09).
X-AUTH-RESOLVER (AP-02, Adopted). "Who is offered what" (AC-R3a-09) evaluates admission for
other reviewers, but the admission service and pool scope take app groups from the current user
and fail closed otherwise (ReviewEligibilityPoolScope.cs:55-60, 79-82; StageReviewService.cs:160-166,
per AP). That is exactly #3251, open since 5 September. New join X-AUTH-RESOLVER: owner the
authorization programme (#3335); the single ProjectAuthorityEvaluator is the natural home
(UNVERIFIED that it can evaluate out of request); needed by AC-R3a-09's per-reviewer rows,
allocation reviewer validity, allocation Phase 2 and AL1. Until it lands, R3a's Monitor shows
pool-level counts by refusal reason with no per-reviewer rows (D3-13d). Evaluating with the
administrator's own groups is the "empty-claims approximation" Chris ruled out for allocation.
RA5 scoped admission (AP-03, D3-13c). Under allocation each bucket holds exactly target
reviewers and EnforceAnnotationTarget refuses a further place; the pure policy returns
AllocationNotAssigned, AtCapacity or AnnotationTargetMet for exactly the requested reviewer
(ReviewEligibilityPolicy.cs:279-292, per AP). So RA5 cannot work on allocated or enforced-target
stages. Recommended (D3-13c): the AdditionalReviewRequest command writes a requestedReview
claim on Study (claim contract v2, E68): single use, expiring, audited; it lets exactly that
reviewer past bucket membership, the pool's at-target filter and the capacity guard; it never
changes the target; it is recorded as provenance on the resulting session and counted outside
allocation progress (E66). It is the same mechanism RT-13 needs (capacity versus target, D3-17).
AL1 placement (AP-10, Adopted). The allocation roadmap has Phases 1–5 (provenance;
authority, revalidation and repair; reallocation and protected claims; materialisation;
switchover). AL1 sits after Phase 2 (it needs the resolver for reviewer validity on a
form-scoped roster) and before Phase 3 (#3745). A form-keyed regime is a regime schema v2
(form binding, target source) with an image floor one release ahead and an updated
StageAllocationRegimeCompatibilityCheck (README.md:194-215, the existing v1 floor). F-A's exit
evidence adds the allocation owner's signed phase mapping. AL1 also needs #3269's legacy-stage
checklist completed on preview or staging (AP-23) and X-AUTH-RESOLVER.
Regime and batch plan by reference (AP-11, Adopted). Following brief §1.12, a canonical
StageSettingsVersion carries allocationRegimeId and batchPlanId; regime and plan records stay
in their own collections. Embedded Stage.WorkloadShares, the regime pointer on the embedded Stage
and #3939's Stage.ProgressiveBatches are legacy-only. PROPOSAL at F3.
Adoption rows (AP-13, Adopted). Migration §3
gains: stage target (override or inherited) → form target, materialised; project agreement
threshold → the compatibility profile's collective rule; an enabled legacy regime → a frozen legacy
regime record, allocation disabled on adoption unless AL1 is live. Criterion AC-R6-14.
Read APIs on canonical stages (AP-15, Adopted). workload-shares/my-studies, /progress and
the editor count "own ordinary saved sessions" from embedded data and gate on ReviewMode.Annotation
(delivery-plan.md:39-61, per AP). Until AL1 they refuse canonical stages with a typed reason, and
the stage overview shows "Allocation is not yet available for shared forms". From AL1 they read the
membership projection (AC-AL1-04..08).
Selection budget and sampling (AP-17, Adopted). Selection is $sample after $match; R3a
adds route filters and #3939 adds an $in of up to 10,000 study IDs per Next. FEAT-008 proposed a
rand-field range scan and a 400 ms p95 target (stage-filtering/README.md:199-237, verified).
AC-R3a-26: selection p95 ≤ 400 ms at 100,000 studies (PROPOSAL, FEAT-008's figure),
measured on Bramble; the sampling strategy is decided at F3 from that benchmark and is part of
X-BATCH's performance gate.
Legacy PartitionSet retirement (AP-20, Adopted). The first L16 PR removes the
study-partitions route, or flags it off; the PartitionSet field stays readable; the inventory
records it as "retire".
Flag double read (AP-22, Adopted). The controller passes the runtime-overridable
RuntimeFeatureFlags value as applyWorkloadShares, while the default TryAdmitActivityReviewAsync
overload reads the process FeatureFlags.ProportionalStudyAllocation
(ReviewController.cs:500, 626, 753, 790, 1173, 1443, 1609; StudyRepository.ActivityReservations.cs:19-21,
per AP). A runtime override can disagree with admission; with #3975 (overrides reset on each
resolution) the disagreement is not hypothetical. Fix in E75: one evaluation per request, passed
into admission, with a test.
Stale STATUS (AP-21, Corrected). The inventory rows are refreshed
(source-status inventory §2,
§3 and
§7). The
allocation owner is asked for a STATUS refresh before G0 (change A6).
3.3 Recommended changes to the existing implementation¶
| # | Change | Rationale | Timing | Compatibility and migration | Risk | PR slicing | Owner |
|---|---|---|---|---|---|---|---|
| A1 | Extract IReviewMembershipFacts; re-point pool predicates, StageWorkloadShareEligibility, AllocationClaimSlot, ActivityReservationAdmission, the own-place checks and the capacity pipelines at it; embedded-Study provider first (E64) |
AP-01, RT-05, RT-06: canonical facts must feed the same policies without a fork | Before F1a freezes E20; no behaviour change | Nothing persisted; the truth table runs row by row against both providers | Low; covered by ReviewEligibilityPoolPredicateTests |
PR 1: Core seam, embedded provider, truth-table parity. PR 2 (R0/R2a): CanonicalSummary provider |
Eligibility owner (seam); L1 (provider) |
| A2 | Refuse ConfigureWorkloadShares on canonical stages; R0 refuses to admit a stage with an enabled regime (E65 part 1) |
AP-06, D3-13a: the evaluator has no valid inputs on a canonical stage | Ships with R0's admission service, before R2a writes | No data change | Low | One small PR alongside the R0 admission service | Allocation owner with L0 |
| A3 | AL1 adapter: regime schema v2 (form binding, target source), floor one release ahead, evaluator reads the form target with an equality check, publication of a different target refused while a regime exists, read APIs and editor on the membership projection, D8 slot rule on form-keyed claims (E65 part 2) | AP-06, AP-10, AP-15, AP-16 | After allocation Phase 2 and X-AUTH-RESOLVER, before Phase 3; F-A, then AL1 | Regime schema bump ⇒ image floor one release ahead; pmStageAllocationRegime keeps unknown elements |
Medium (floor, start-up check) | PR A: schema v2 and floor. PR B: evaluator on form target. PR C: editor and read APIs | Allocation owner with L7 |
| A4 | requestedReview claim honoured by pool filters, allocation, typed admission and capacity guards (E66) |
AP-03, RT-13: RA5 otherwise fails on allocated or enforced-target stages | Design at F4; build in R4a | Additive claim kind and session provenance, behind R4a's flags | Low to medium | One L6 PR touching ActivityReservationAdmission and the policy facts (AdditionalReviewAdmitted) |
L6 with eligibility and allocation owners |
| A5 | Out-of-request authority resolver (#3251) on the authorization programme's evaluator | AP-02: blocks reviewer validity, AL1 and per-reviewer preview | X-AUTH-RESOLVER: before AL1 and before R3a's per-reviewer rows | Read-only evaluation | Medium (semantics across identity providers) | Authorization programme PR; allocation Phase 2 PR consumes it | Authorization owner; allocation owner |
| A6 | Refresh STATUS (#3603 merged, #3048 closed, 7f8b9f4c1), and record that the flag reaches the API host only |
AP-21, AP-08 | Before G0 | None | Low | One docs PR | Allocation owner |
| A7 | Retire the study-partitions route (keep PartitionSet readable) |
AP-20 | First L16 PR | Route removal only | Low | One web PR | L16 |
| A8 | One flag evaluation per request, passed into admission (E75) | AP-22 | Before any environment enables eligibility with allocation | Code and a test | Low | One eligibility-programme PR | Eligibility owner |
| A9 | Run #3269's legacy-stage acceptance on preview or staging | AP-23: AL1 builds on an unaccepted base | Before F-A | None | Low | Acceptance run, no code (refresh or replace #3327's preview vehicle) | Allocation owner; L17 |
3.4 Do not build further until¶
- Allocation Phases 3–5 (reallocation, protected claims, materialisation, switchover) against embedded Study: until E20 (membership projection) and claim contract v2 freeze at F1a and X-BATCH freezes at F3 (AP-01, AP-07). Building them now means a second rewrite.
- Materialised per-reviewer allocation counters (the evidenced next step for #3252's admin read): until F1a decides whether they are a FEAT-024 family or a read of the membership projection. They must never become a third counter store (OPS1: "no competing counters").
- Any allocation exposure on canonical stages before AL1 (D3-13a).
3.5 What the plan adopts¶
- The regime pattern (immutable record, transition log, stage pointer, start-up compatibility
check) as the template for
StageSettingsVersionand batch plans; #3603's project-revision fence as the StageSettings publication fence. - The review-eligibility truth table as the conformance format for C6 and for E64 (also PH-04).
- Deterministic bucket assignment (FNV-1a mod 10,000 on study ID) as AL1's allocation primitive: no per-study write when a plan changes.
- Chris's reviewer-validity requirement (22 September) applied to AL1's form-scoped roster.
4. Progressive shared review batches¶
FEAT-026, owned by the batch programme: plan #3936, implementation #3939.
4.1 Current state¶
| Component | State | Evidence | Gaps |
|---|---|---|---|
| #3936 plan | OPEN, MERGEABLE (blocked on review), head a799b538e, 6 files +401/−11, updated 2 October 17:21 UTC (DOC-DRAFT) |
gh pr view 3936; gh pr diff 3936 |
Records behaviour it attributes to Chris and three open decisions (denominator, late arrivals, reconciliation) |
| #3939 implementation | OPEN, CONFLICTING, head 62e8101eb, 63 files +3,233/−60, updated 2 October 19:21 UTC (CODE-PR) |
gh pr view 3939; gh pr diff 3939 |
Adds Stage.ProgressiveBatches (embedded), three collections (plan, membership, access), and a PM-only flag block progressiveReviewBatchBackendFlags delivering progressiveReviewBatches and reviewEligibilityPolicy; ConfigureAsync refuses unless both flags are on |
| Completion definition | Legacy-keyed: stage tally NumberOfCompletedCandidateSessions against stage.SessionCountTarget, ReviewMode and the project threshold; every project study enrolled (Filters.InProject) |
#3939 ProgressiveBatchConfiguration.cs, ProgressiveReviewBatches.PrepareAsync |
Diverges from SF2/R3b for shared forms; later stages' denominators include studies that cannot enter them |
| Shared opening | Recomputed lazily in RefreshAsync on every Next and status read; personal grant is a $max on the access row; nothing emitted, audited or notified. #3936 required a CAS opening plus an outbox |
#3939 ProgressiveReviewBatches.cs; #3936 technical plan "Concurrency, lifecycle and migration" |
Amendment A's pool-entry event has nothing to attach to |
| Performance | "Arrival detection still scans current project study IDs with an indexed anti-join; production-scale performance qualification remains a rollout requirement." The 2,500-batch test checks an index plan only | #3939 README diff | Unqualified |
| Denominator decision | #3939's README states "Chris confirmed this disposition" (excluded studies stay in the denominator and count as finished; restored work returns to its original membership) | #3939 README diff, verified | Not in the ledger or the register (grep, verified) |
4.2 Integration with the plan¶
The plan's join was "X-BATCH: #3939 merged or completion definition extracted" → R3c, and
AC-R3c-05 required readiness "to give the same result as #3939's for the same data". Under SF2 (one
form-owned target, one contribution across stages) and R3b (per-profile outcomes) that criterion is
unsatisfiable for shared forms (AP-04). X-BATCH is rewritten (Corrected) as six entry criteria,
which hold before R3c reuses batch readiness and before any environment enables
progressiveReviewBatches:
- Evidence seam (AP-04). Completion becomes a pure decision over
IStudyObligationEvidencefacts (E67). A legacy provider reproduces #3939's current definition; a canonical provider reads the form-unique membership projection (E64) and, from R3b, per-profile outcomes. AC-R3c-05 is reworded: "uses the same definition (seam) and the shared fixtures; results differ only where SF2 or R3b semantics differ, and those cases are listed". - Pool-entry-based denominator (AP-04). Membership and denominators are defined over studies
that entered the stage's pool (amendment A's
StudyEnteredPoolevent inStudyPoolLedger), with late entrants appended as separately shuffled cohorts (#3936's proposal), instead ofFilters.InProject. From R3a, route filters decide who can enter. - Durable opening transition (AP-05). A shared opening is a compare-and-swap on the plan (plan ID, shared ordinal, revision) and a personal grant a compare-and-swap on the access row. Each writes a pool-entry event (or its legacy capture, E26) and a durable intent for audit and notices (C19 class b) in the same transaction. Transaction rows "batch opened" and "personal batch grant" go to the consistency model.
- Status reads never mutate (AP-05).
GetStatusAsyncreads; openings happen only in commands (a completion hook or an evaluator triggered by evidence change). - Performance gate (AP-05, AP-17). Comparable to #3252: Next p95 at 100,000 studies and 2,500 batches with documented keys examined, bounded opening-evaluation cost, and AC-R3a-26's selection budget, on Bramble, before any environment enables the flag.
- X-ELIG first (AP-19). Batches require
reviewEligibilityPolicy; the legacy flag-off Next path has no batch integration. Never enable batches in an environment before X-ELIG holds there, with both flags delivered to both hosts (E75).
PRISMA pool entry (D3-13e). Recommended: a study "enters screening" at its first release to anyone; shared openings and personal grants are recorded as separate pool-entry sources, so early-stopped and batched reviews report honestly (amendment A).
Denominator (AP-12, D3-13b). Chris is asked to record #3939's denominator disposition in the
ledger (the ledger owner assigns the ID) together with "restored scope returns to its original
membership". Until then #3939's README text is PROPOSAL.
Slicing (D3-13f). Recommended: merge #3939 in slices: PR 1 pure completion and progression decisions behind the evidence seam, with truth tables; PR 2 persistence with durable opening and grant transitions emitting pool-entry events; PR 3 Next integration and direct-access checks, held until the performance gate; PR 4 settings UI to Material 3 (UI1). Never activate before X-ELIG in the same environment.
Double ownership of settings (AP-11). For canonical stages the StageSettingsVersion carries
batchPlanId (§3.2). #3939's rule "disable the stage before changing batches" maps onto R3c's
Completed/Reopen lifecycle, not onto a disabled stage.
Supersession rows (AP-18, Adopted). The inventory's §6 gains FEAT-026's "Do not add a stage
closure concept" (superseded by LC1/RX2) and FEAT-008's Filter Set model (decide at F3 whether the
Filter Set schema is the route representation; keep the same-profile $elemMatch simplifier as a
correctness rule for screeningOutcomes[] filters).
Batches and FEAT-024. #3936 allows materialised rows only "if their contract matches exact batch scope and configured targets". FEAT-024's annotation families classify with a fixed two, so batches use authoritative indexed completion (as #3939 does) until X-STATS-c (§7).
4.3 Recommended changes to the existing implementation¶
| # | Change | Rationale | Timing | Compatibility and migration | Risk | PR slicing | Owner |
|---|---|---|---|---|---|---|---|
| B1 | Split #3939 into the four slices above | AP-04, AP-05, AP-11, AP-19; D3-13f | Now, before X-BATCH freezes at F3. PR 1–2 may merge dark once reworked; PR 3 waits for B5 | Three collections are additive; Stage.ProgressiveBatches legacy-only |
Medium: the PR conflicts and its frontier is O(N) per Next | Four PRs | Batch owner |
| B2 | IStudyObligationEvidence seam with a legacy provider (E67) |
AP-04 | PR 1; canonical provider in R3c | None | Low | Part of PR 1 | Batch owner; L4 supplies the canonical provider |
| B3 | CAS opening and personal grant writing pool-entry events and durable intents; status read-only | AP-05 | PR 2; before any enablement | New fields on plan and access rows; intents per C19 | Medium | PR 2 | Batch owner with L12 (event shape frozen at F3) |
| B4 | Pool-entry-based membership with late cohorts; route-aware enrolment from R3a | AP-04 | PR 2 (legacy pool = project minus excluded); canonical from R3a | Membership rows additive; adoption re-bases membership on pool-entry events | Medium | PR 2 | Batch owner |
| B5 | Performance qualification on Bramble | AP-05, AP-17 | Before PR 3 merges and before any enablement | None | Low | Benchmark PR | Batch owner; L17 |
| B6 | Record the denominator decision | AP-12, D3-13b | Before PR 2 merges (opening depends on it) | None | Low | Ledger update | Chris; ledger owner |
| B7 | Deliver both flags to both hosts through one block, replacing #3939's partial PM block | AP-08, E75 | With X-ELIG | Generator run only | Low | Eligibility-programme PR | Eligibility owner; batch owner reviews |
4.4 Do not build further until¶
-
3939's selection integration (Next, direct access, lists) against embedded Study: until X-BATCH¶
freezes at F3 and B5 passes. - Any reconciliation batching mode (#3936 open decision 3): until R4a's task model (one task per study × form, RE4) exists.
4.5 What the plan adopts¶
- The behaviour #3936 records as agreed with Chris (
DOC-DRAFT; not yet in the ledger): random, stable, shared membership with a persisted seed; configurable size and threshold; automatic personal advancement that never opens a shared batch; return to earlier work when it becomes eligible; the two saved-session policies. - Monotonic access, append-only cohorts, and "a batch grants scheduling access, never permission" (consistent with C10).
- The copy rule "reviewer exhaustion is never 'Stage complete'" in the copy deck (UX strategy).
ProgressiveBatchCompletionas R3c's readiness definition, through the seam.
5. Review eligibility policy¶
Owned by the eligibility programme; paused (see 5.1).
5.1 Current state¶
| Component | State | Evidence | Gaps |
|---|---|---|---|
| Merged slices | Dark behind reviewEligibilityPolicy: settings and claims #3579, S1a #3646, S1b #3659, S2 #3691, S2-C #3695, S3 #3719, S4-A #3732, claim-revoked event with durable intent #3736 (24–25 September) |
gh pr view (all MERGED) |
— |
| Open slices | #3746 S6a (server-contract residue, flag-off identity): OPEN, CONFLICTING, updated 1 October. #3742 S4-B (D7 migration tooling): draft, 0 files. #3741 S5 audit: draft, 3 files. S4-C (settings API and editor) and S6b (browser consumption) have no PR | gh pr view; review-eligibility-policy.md:36; #3746 body ("The web doesn't consume them yet (S6b)") |
— |
| Flag | Default false; unset everywhere; delivered to the API host only (env-mapping.yaml:1150, entry :1243) |
Verified | Core code that branches on it runs in both hosts; #3939 had to add a PM block. Whether any PM-hosted writer branches on it today (for example #3695's fences) is UNVERIFIED |
S5 audit (branch, DOC-DRAFT) |
The browser never reads eligibility; #3551's veto remains; two revocation causes; no E2E or staging acceptance; fixed-two "not done" (D3a.2); D8(2c) deferred to #3745 |
AP §2, citing the audit branch | — |
| Hotspot | With the flag on, every review write also writes Project.StatisticsAdmissionToken ("the same real project-token write the reviewer save uses"); measured with statistics off: 199 of 500 submissions exhausted at five reviewers on different studies, 700 of 1,000 at ten |
StudyRepository.ActivityReviewWrites.cs:87-91 (verified); STATS screening-write-benchmark.md:549-554 (per DC-05) |
R3a would inherit it |
| Pause | "Paused 25 September until the statistics work finishes" is recorded only in session memory; the policy document (updated 25 September) does not say so; #3746 had activity on 1 October | UNVERIFIED in the repository; A-31 | — |
5.2 Integration with the plan¶
X-ELIG enumerated (AP-09, Corrected). X-ELIG listed the flag, the reservation migration and
the D7 tool. It now lists each slice with an owner and a "merged or extracted" criterion:
| Item | What | Criterion |
|---|---|---|
| S4-B | Audited D7 migration tooling (dry-run plan, CAS apply, rollback, audit), targeting claim contract v2's key (E68) | Merged and rehearsed, or replaced by an authorised count-only check showing nothing to migrate (A-32) |
| S4-C | Grouped-configuration settings API and editor | Merged, or absorbed by R3a's stage designer for canonical stages (legacy stages keep today's editor) |
| S6a | Server-contract residue and flag-off identity (#3746) | Rebased and merged |
| S6b | Browser consumption of the eligibility response (removes #3551's veto) | Absorbed by R3a as part of AC-R3a-03 if the programme is still paused at F3 (D3-09) |
| Fixed-two correction | S5 row D3a.2; FEAT-024-owned for the statistics families | X-STATS-c (E71) |
| Project-token redesign | Admission never writes a per-project document per save (DC-05, MS-19) | Agreed with the eligibility owner at F3; AC-ALL-26 (C18-T02) with eligibility on |
| Flag delivery | Both flags reach API and PM with a cross-host agreement check (AP-08) | E75 merged |
D8 mapping (PH-04, D3-09). Eligibility D8 (Chris, 24 September) has five sub-decisions; none was mapped. Recommended mapping, put to Chris in D3-09:
| D8 sub-decision | Mapping in the step model |
|---|---|
| (1) Removing own annotation on a disabled stage or after a mode change is allowed "until annotation versioning replaces deletion" | Legacy scopes unchanged. Canonical sessions are never deleted: the action becomes withdrawal or audited draft discard (C5); D8(1)'s own sunset clause applies |
| (2) Leftover claims outside the reviewer's allocation are released with a warning | Carried into claim contract v2: the release is typed admission's job; under reallocation (#3745, allocation Phase 3) claims count as protected work. AL1 inherits it |
| (3) A disabled stage cannot be opened | Holds for the stage route. A shared session stays reachable through another active bound stage (SF1); the refusal is per route, not per session |
(4) Hidden excluded saved work mirrors HideExcludedStudiesFromReviewers |
Applies per route; EW1 (saved-work completion after collective exclusion) governs canonical stages, with the hide setting presentation-only |
| (5) An administrator may reconcile before readiness, with a warning | Carried into R4a as a non-blocking ReconciliationNotReady warning on the task; readiness still gates the reconciliation pool |
The truth table (generated, with Mongo-seeded row tests) is extended with step and route columns rather than replaced by a separate C6 table (PH-04).
Flags delivered API-only (AP-08, Adopted). reviewEligibilityPolicy and
proportionalStudyAllocation sit in the API-only block. Enabling one host but not the other is the
split-brain hazard FEAT-024 guards against with its durable reviewer-mode epoch. E75: move both to a
block delivered to API and PM, add a cross-host agreement check (durable mode, as for tracking),
inventory PM-hosted branches, and record in the inventory that both are API-only today.
Project-token hotspot (DC-05, MS-19, Corrected). R3a's admission "extends
ReviewEligibilityPolicy", whose save path writes the Project token per review write. Canonical
admission must never write a per-project document per save. Default design (PROPOSAL, chosen at
F3 with evidence): admission checks the bound StageSettingsVersion (its ID and revision) in the
Study write filter, and a settings change publishes a new version under the engine's scoped fence
with a drain, so in-flight admissions either see the old version and commit before the fence or
retry against the new one. FEAT-024's fold deferred item 2 (async-point-fold-design.md:1899-1902)
is handed to L4/C6 at F3. The write gate (AC-ALL-26, C18-T02) runs with eligibility on.
What R3a absorbs (D3-09). If the programme is still paused at F3: S6b's browser consumption (as part of AC-R3a-03), and S4-B, S4-C and S6a only as far as R3a needs them for canonical stages. If it resumes, those slices return to it and R3a's scope shrinks (A-31). Either way the eligibility owner's C6 checklist passes at F3, run by a fresh-context agent; Chris rules on exceptions.
One reservation migration (AP-07, Adopted). E18 re-keys claims to study + form (+ profile) +
reviewer; X-ELIG already needed a migration of untyped reservations. Two migrations would touch the
hub, the consumers and the FEAT-024 fold twice. One migration (S4-B as the vehicle) targets claim
contract v2's final key with stage provenance (E68); see §6.2 for its consumer inventory. RT-23
adds that production probably holds no untyped reservations at all, because claims exist only
with tracking on (A-32); an authorised count-only check per environment replaces a migration
rehearsal where the count is zero, and eligibility is switched on before tracking.
RA5 (AP-03). The pure policy's facts gain AdditionalReviewAdmitted (E66, §3.2).
5.3 Recommended changes to the existing implementation¶
| # | Change | Rationale | Timing | Compatibility and migration | Risk | PR slicing | Owner |
|---|---|---|---|---|---|---|---|
| L1 | Membership-facts seam (A1, E64) | AP-01 | Before F1a | None | Low | As A1 | Eligibility owner |
| L2 | Deliver both flags to both hosts; cross-host agreement check; inventory PM branches; one evaluation per request (E75) | AP-08, AP-22 | Before any environment enables eligibility | Helm mapping and generator only; no data | Low | One PR (replaces #3939's partial block) | Eligibility owner |
| L3 | S4-B targets claim contract v2's key; count-only check first | AP-07, RT-23, A-32 | Key frozen at F1a; executed with X-ELIG | Migrates untyped → typed and stage → form keys once; FEAT-024 reservation fold kinds bump the protocol (IntroducedAt) |
Medium (hub, consumers, fold) | S4-B; FEAT-024 protocol change separately | Eligibility owner; FEAT-024 owner; presence owner |
| L4 | Replace the Project token with a StageSettings-version check for canonical admission | DC-05, MS-19 | Design at F3; build with R3a | Behind the flags; legacy path unchanged until migrated | Medium | One PR in R3a (or the eligibility programme if resumed) | L4 with eligibility owner |
| L5 | Extend the truth table with step and route columns; encode the D8 mapping | PH-04, D3-09 | F3 | Generated table and row tests | Low | One PR | L4 with eligibility owner |
| L6 | Rebase #3746 (S6a) or close it in favour of R3a | AP-09 | Before F3 | — | Low | Owner's call | Eligibility owner |
5.4 Do not build further until¶
- S4-B's migration of legacy untyped reservations: until a count shows any exist (RT §4 hold list) and claim contract v2's key is frozen.
- New
ReviewActivityvalues on v1 typed pages: until claim contract v2 freezes.
5.5 What the plan adopts¶
The facts → policy → decision pattern with stable reason codes; the generated truth table and its Mongo-seeded row tests; typed refusals with bounded server-authored details (#3746); the claim-revocation outbox and targeted event (#3736) for every revocation; guarded settings saves with "Apply anyway" (#3579) as the D6 conflict flow.
6. Active reviewer tracking, claims and presence¶
Owned by the presence owner (named at G0; the tracking PRs so far — #2467, #3008, #3014, #3719 — came from Chris's sessions).
6.1 Current state¶
| Fact | Evidence (verified unless marked) |
|---|---|
The effective mode is ActiveReviewerTrackingAvailable = ActiveReviewerTrackingEnabled && SignalRActive |
src/libs/kernel/SyRF.SharedKernel/Settings/FeatureFlags.cs:35-36 |
Claims, capacity guards and typed admission exist only when tracking is effective. With it off, no claim is created, annotation and screening saves are unguarded and EnforceAnnotationTarget does nothing |
RT-02 (StageReviewService.cs:220-221, 331-335, 631-634; SubmitAnnotationSessionService.cs:321-337; StudyRepository.cs:2056-2073, 2523-2525) |
| Off in every deployed environment (unset in production, staging and preview) | cluster-gitops ea972fd6: no activeReviewerTrackingEnabled key; appsettings default false (API appsettings.json:33, PM :35) |
| On in the E2E stack | appsettings.e2etest.json (API :130, PM :83) |
| Delivered to API and PM | env-mapping.yaml:1593 (services: [api, project-management]) |
| Reconciliation is excluded at every layer: it reserves nothing and resolves no capacity mode | StageReviewService.cs:67-70, 153-156; presence policy turns off in reconciliation mode (stage-review-presence-policy.ts:17, per RT) |
Turning it on is a FEAT-024 durable reviewer-mode transition (capacity writes fail closed with a typed 503 on disagreement); AdvanceModeEpochAsync has no production caller (M15) |
.claude/rules/materialized-stats.md "Durable reviewer-mode epoch"; ProjectStatisticsWriteEpochLifecycleService.cs:370 (only tests call it) |
| Every surface is keyed by stage: reservation natural key (InvestigatorId, StageId), the two-activity page, connections, the unique current-presence index, hub signatures, DTOs, scheduled commands (held in Quartz up to 2 h) | RT §2 and RT-11 |
| PM ignores runtime flag toggles | #3360 OPEN |
| Per-stage tracking was deferred from the fold MVP by Chris on 1 October | #3876 OPEN (body: "Decision: Chris, 2026-10-01: defer from the MVP and do it as a follow-up") |
| Production over-allocation report still open; synthetic coverage only | #2446 OPEN; #2565 CLOSED; epic #2470 OPEN |
Design documents are stale: the feature page's narrative still describes ActiveReviewSession; the redesign document is Draft (April) |
docs/features/signalr-active-reviewer-tracking.md:102-160; review-session-model-redesign.md front matter |
6.2 Integration with the plan¶
X-RECLAIM replaces X-TRACK (RT-01, Blocker, Corrected). RA1 (OWNER): "Normal pool work
uses existing active-work tracking to prevent two reconcilers simultaneously editing the same
study." Tracking gives reconciliation no protection at all, yet the plan accepted "tracking enabled"
as an R4a production route and had no criterion testing editor exclusion. X-RECLAIM is a
reconciliation-task editor claim (CAS plus lease on the task, atomic "Start reconciling", assignment
and release hooks for RA3/RA4) that works with tracking off. Owner: L6 with the presence owner;
frozen in C9/C7 at F4; required for R4a in every environment. E6 is reworded from "if tracking
stays off" to "always". Criteria AC-R4a-36 to 39. It is the RA1 mechanism; "tracking
enabled" is no longer a sufficient R4a prerequisite. The editor claim is the taskEditor kind of
the claim contract (query review reuses it as queryEditor, AC-R4b-09).
Q-25's tracking line was wrong (RT-02, Corrected; D3-16). Q-25 said tracking is "not needed for
slot-reservation claims". The reverse holds: claims exist only with tracking on. So R2b's claim
re-keying, R3a's reservation admission and every capacity promise do nothing in production. Q-25
keeps its decision; its tracking cell gets a correction note
(open questions, Q-25 correction), and the production
claims route goes back to Chris as D3-16. Recommended (a): reshape #3876 into a binding-scope
tracking setting and enable it per admitted pilot project (and for legacy projects that opt in,
addressing #2446).
X-CLAIMS (RT-02, RT-03, Adopted). New production prerequisite for R2b claim behaviour, R3a's
reservation admission and any capacity promise. Jointly owned by the presence owner and the
FEAT-024 owner. Evidence: (a) the route D3-16 chooses is built: the binding-scope setting enabled for
the pilot, or the M15 transition wired and rehearsed on staging; (b) API and PM switched together in
one static configuration change (#3360); © load and failover runs AC-T-03 to AC-T-07; (d) the orphan
backstop AC-T-06; (e) the affected E2E flows pass in both tracking modes (AC-ALL-23). Staging cannot
rehearse a fleet-wide switch today because FEAT-024 writes are on there; whether the global control
row exists in staging is UNVERIFIED.
Claim contract v2 at F1a (RT-11, Adopted; E68). A v1 typed page holds one screening and one
annotation claim, but R3a stages can hold several form and profile steps, and a claim shared by two
stage tabs must live until the last tab closes. The contract (value object in the
domain model):
- A claim is
{kind, scopeId, routeStage, routeStep, reservedAt, allocationRegimeId, leaseExpiry, holders}; kind ∈formSlot,profileSlot,requestedReview,taskEditor,queryEditor; unique per (study, kind, scope, reviewer); released when the last page using it ends. - Capacity claims (
formSlot,profileSlot,requestedReview) live on Study, so the atomic guard still works; editor claims live on their own aggregates (ReconciliationTask, QueryWorkItem). - Claims key on form identity, never form version.
- Versioning: new hub methods (for example
JoinReviewContext) rather than new parameters untilMinUiVersionmoves (NotificationHub.cs:109); additive DTO fields; new command contracts, with the old handlers kept for at least the suspension grace (2 h) plus the idle timeout; presence index migration by create, read both, drop. Legacy v0/v1 pages stay for legacy scopes. - One reservation migration with the eligibility programme's S4-B (AP-07). Consumers of the
(InvestigatorId, StageId) key, each with its cutover: pool predicates, the D8 slot rule
(
AllocationClaimSlot), typed admission,Study.GetSlotReservation, the FEAT-024 reservation fold kinds (protocol bump withIntroducedAt),pmReviewerPresence's unique index, the hub join, the idle, suspension and liveness consumers and their scheduled commands, and claim-revocation intents.
R0 floor and inventory (RT-07, RT-08, Corrected). SessionTallies are recomputed on every
load (stored values ignored) and the claim pipeline recomputes the allocated total as candidates
plus reservations (ExtractionInfo.cs:31-80; StudyRepository.cs:2913-2916, 3217-3227, per RT and
DC-02). A binary that does not merge canonical counts writes them away on any whole-Study save or
claim. R0 therefore ships reader logic, not only tolerant maps: the tally getter and the claim
pipeline merge CanonicalSummary's form-keyed counts and per-reviewer markers, inert until a
canonical writer exists (mechanics: consistency model); AC-R0-09. The R0
writer and reader inventory gains tracking's Study writers (hub join, leave, dirty/clean,
disconnect and prior-study release; PM idle, suspension and liveness consumers; the claim pipelines
and typed admission; the direct-navigation claim; the screened-reservation release; the guarded
settings save's "Apply anyway"; the reservation restore on session deletion) and readers (the
presence snapshot; FEAT-024's availability calculators), each with a route, refuse or adapt
decision. AC-R0-02 gets one test per writer, including "a stage-keyed claim on a canonical form is
refused or translated".
R2a is not claim-free (RT-05, Corrected). A-21's basis ("keeps R2a free of claim, tally and
allocation joins") is rewritten. R2a keeps claims stage-keyed (one stage per form) but must change:
own-place detection (through E64, so a returning reviewer with a canonical session is not treated as
new); claim release on the first explicit Save or Complete, inside the canonical transaction;
presence FormSessionId (additive; legacy field kept); and "dirty" meaning C5's draft-changes flag,
not AF2's pristine state. At F1a the presence owner's checklist for the R2a adapter passes, run by a
fresh-context agent; Chris rules on exceptions. AC-R2a-35 to 37 and AC-R2a-06.
Draft holds the place? (RT-09, D2-07). RT recommended "yes until Save or Complete, discard, admin release or 14 days"; PH-03 said "never". The adjudicated middle ground (brief §1.8, D2-07): a draft keeps the place while the reviewer is active, under today's idle and disconnect timers counting draft activity; when they lapse the place is released but the draft is kept; the reviewer may still Complete it as an extra contribution (SF4's target is a minimum) unless an optional capacity cap applies (D3-17), in which case they are told honestly and may keep or discard the draft. Release paths read whether a draft exists in the same snapshot; autosave never writes Study.
Draft lease on connection identity (RT-10, Adopted; D2-08). The lease is held by a stable
client tab ID (sessionStorage) recorded on both the draft and ReviewSessionConnection, renewed by
a REST heartbeat (the bulk-PDF lease pattern, BulkPdfUploadController.cs:368-375) or the hub
heartbeat when tracked. It works with tracking off. Other tabs are read-only, with an explicit
"Take over editing" that ends the old lease (D2-08). Draft mechanics (etag, write sequence, conflict
copy) are in the consistency model.
Capacity versus target (RT-13, D3-17). SF4 makes the form target a minimum; today one number is
both minimum and cap. Recommended (D3-17): an optional capacity cap (stage or route policy), off by
default; when on it defaults to the form target, never limits requested extra reviews, never evicts
existing work. Sessions that hold a place: a draft-backed claim while active (D2-07), saved
incomplete, completed, Needs updating; withdrawal frees the place. The requestedReview claim lets
exactly the requested reviewer past the pool and capacity filters (AP-03, E66).
Shared-form tracking settings (RT-12, D3-18, extends Q-28). EnforceAnnotationTarget,
IdleSessionTimeoutMinutes, MaxInProgress and #3876's proposed setting have no rule for shared
forms. Recommended (D3-18): the most restrictive bound stage sets the cap and the idle timeout; the
stage in use sets the in-progress limit, counting a shared session once; the form is tracked if any
bound stage is. #3876 is shaped as a binding-scope setting, not a per-stage one. AC-R2b-09, AC-R2b-10.
Dependent steps (D3-19). Recommended: no place is held on a dependent form while the reviewer is still screening; the claim is taken at Include; if refused, the reviewer sees "Enough reviewers" for that step and keeps their screening decision (matches D6's "acquire only currently eligible activities"). AC-R3a-25.
Presence disclosure (RT-14, D3-20). Presence snapshots carry the investigator ID of every
reservation holder to every member who can view studies, whatever the stage's blinding
(StudyReviewPresenceSnapshot.cs:86-96, per RT). Realtime presence is added to C10's channel list
(Adopted). Recommended disclosure rule (D3-20): reviewers see counts and their own place; names
only for holders of the Monitor capability; never across reconciliation blinding (BL1). AC-T-08.
E2E in both modes (RT-04, Adopted). E2E runs tracked; production runs untracked; "existing
specs pass with flags off" proves the wrong behaviour. AC-ALL-01/02 state the tracking mode, and the
affected review-flow specs run in both modes as a Playwright project matrix for R2a, R2b, R3a and
R4a (AC-ALL-23).
Orphan backstop (RT-24, Adopted). A lost scheduled removal (Quartz has lost its RabbitMQ
connection after a pod restart before) leaks a claim indefinitely, and claims made dirty and left
when the flag is turned off count again when it is turned back on. Before X-CLAIMS: claims carry an
absolute leaseExpiry that guards treat as free once passed, or a bounded sweep runs behind its own
flag. AC-T-06.
Completion, revocation and target reduction (RT-18, RT-19, RT-20, Adopted). Stage completion
withdraws that stage's claim references through the claim-revocation outbox (#3736); the claim
survives if another bound stage still uses it; the client keeps its unsaved changes (AC-R3c-14).
Publishing a lower target, or switching enforcement on, goes through D6's conflict flow ("Apply
anyway"), revoking the most recently acquired claims first (AC-R2c-19). Revoking a grant releases
temporary claims through the outbox; drafts are kept (AC-R1c-10).
Retention (RT-21, Adopted). pmReviewerPresence ("preserved indefinitely") and
pmReviewSessionConnection join E32 with retention rules, and join adoption manifests if they
become exposure evidence (retention text in
open questions E32).
Stale documents (RT-27, Adopted). Before F1a the presence owner lands a docs-only PR (T1).
Other tracking rules. Transaction rows for claims and presence (RT-15) go to the
consistency model: the first explicit Save/Complete also releases the claim
and closes and opens presence; claim, release and expiry write the Study claim set plus the
FEAT-024 part or fold entry; task claim and release write the claim plus assignment state; the
pinned command-budget tests (FoldSaveCommandBudgetTests, ProjectStatisticsFoldCommandBudgetTests)
are named for update. AC-R2b-03 is rewritten (RT-16): two tabs through stages A and B hold one claim;
closing either keeps it; closing both releases it once. Q-20's publish-pause warning needs a
form-scoped "who has form F open now", which needs presence keyed by form and tracking on; the
minimal version drops the warning (RT-17). The copy deck gains the slot vocabulary ("review slot",
"released", "offline", "enough reviewers") and the draft/claim rule (RT-22,
UX strategy). No notice per claim; one workload notice per reviewer per plan change;
revocation events keyed by claim scope (RT-25, C15). Exposure reports travel in the draft, Save and
Complete REST payloads, never over the presence hub, which is off in production (RT-26, C3).
6.3 Recommended changes to the existing implementation¶
| # | Change | Rationale | Timing | Compatibility and migration | Risk | PR slicing | Owner |
|---|---|---|---|---|---|---|---|
| T1 | Update the delivered contract: claims only when tracked, typed claims, reconciliation excluded, FEAT-024 mode coupling, stage-keyed surfaces; mark the redesign Implemented; retire the April planning documents | RT-27; Q-25's error came from stale documents | Now, before G0 | None | Low | One docs PR | Presence owner |
| T2 | Give M15 an owner: expose the reviewer-mode transition (AdvanceModeEpochAsync, the epoch batch, the 5-minute grace) through an admin route, with a runbook and staging rehearsal |
RT-03: no environment with FEAT-024 writes can turn tracking on today | Only if D3-16 chooses (b); before any tracked pilot on staging | Fleet-wide invalidation then family rebuild; static configuration on both hosts (#3360) | Medium | (a) route and batch wiring; (b) runbook and rehearsal | FEAT-024 owner with presence owner |
| T3 | Reshape #3876 into a binding-scope tracking setting (effective per stage-settings binding; tracked if any bound stage is) | RT-03, RT-12: per-project pilots without a fleet-wide transition; works for shared forms | Design at F1a with E68; build before X-CLAIMS (D3-16 a) | Default keeps today's behaviour; a change is an ordinary scoped statistics invalidation | Medium | (a) setting and read paths; (b) scoped invalidation; © settings UI | Presence and FEAT-024 owners |
| T4 | R0 floor: tally getter and claim pipeline merge canonical counts and markers; claim and presence writers check canonical ownership (E70) | RT-07, RT-08 | R0, before R2a writes | Inert until a canonical writer exists; tests interleave R0 and R2a writes | Medium (hot path) | (a) getter merge; (b) pipeline; © ownership checks in tracking writers | L0/L1 with presence owner |
| T5 | R2a adapter: own-place detection via E64; claim release on first explicit Save in the canonical transaction; presence FormSessionId; dirty = draft-changes flag; draft-aware release per D2-07 (E70) |
RT-05, RT-09 | R2a | Behind R2a flags; additive presence field | Medium | (a) server release and own-place; (b) presence link; © client dirty semantics (L5 order) | Presence owner with L1/L5 |
| T6 | Claim contract v2: versioned hub methods, DTOs, commands; presence index migration (E68) | RT-11 | Freeze at F1a; build against fakes in W1; ship with R2b | New hub method beside the old ones; additive DTOs; old handlers kept ≥ 2 h + idle timeout; presence index create–read-both–drop | High | (a) domain claim set and Study class map; (b) canonical claim pipeline and guard; © hub and DTOs; (d) commands and consumers; (e) presence and connection keys plus index; (f) web store | Presence owner |
| T7 | Draft lease on connections: stable tab ID, REST heartbeat, take-over (E70) | RT-10; must work untracked | Contract at F1a; build in R2a | New fields on ReviewSessionConnection (already tolerant of extra elements) |
Medium | (a) tab-ID plumbing; (b) REST lease endpoints; © take-over UI | L1/L5 with presence owner |
| T8 | Reconciliation task editor claim (CAS plus lease), atomic "Start reconciling", assignment and release hooks; requestedReview claim on Study |
RT-01, RT-13 | Contract at F4; build in R4a | New aggregate fields; the reconcile host joins presence through a new method | Medium | (a) task claim and lease; (b) atomic start; © assignment interplay; (d) requested-review claim; (e) host states | L6 with presence owner |
| T9 | Orphan-claim backstop plus a load and failover run on Bramble (E69) | RT-24; scale unknown | Before X-CLAIMS | None | Low to medium | (a) lease expiry or sweep behind its own flag; (b) benchmark | Presence owner |
| T10 | Shape presence payloads by the disclosure rule (counts plus the recipient's own claim) | RT-14, D3-20 | Before any tracked pilot with blinding, and before task claims reach presence | Additive; the client still finds its own claim | Low | One PR | Presence owner with authorization owner |
| T11 | E2E in both tracking modes (E69) | RT-04 | Before R2a acceptance (W2) | None | Low | Playwright project matrix | L17 |
6.4 Do not build further until¶
Claim contract v2 freezes at F1a: #3876 as a per-stage setting; any new ReviewActivity value on v1
pages; the admin presence dashboard (#2470's admin-visibility item, redesign phase 6) on stage-keyed
presence; analytics on ReviewerPresence.AnnotationSessionId; in-place hub signature changes; the
migration of untyped reservations in #3742 until a count shows any exist.
6.5 What the plan adopts¶
ADR-008's absolute server timestamps and shared REST/hub access result for every new timer (RA3
expiry, draft retention, leases); generation tokens and baselines that reject stale scheduled
deliveries; AccessDenialReason extended with append-only values (step locked, stage completed, task
held, place held by draft) rather than a parallel enum; the claim-revocation outbox (#3736) for every
revocation; the version-guarded reservation save with its statistics part
(ReservationChangeSave, #3738) for every claim write; the fail-closed pattern for fleet-wide modes; the suspended-sessions
handover's "no false positives" criteria (docs/planning/session-capacity-suspended-sessions-handover.md:291-299).
7. Materialised statistics¶
FEAT-024, owned by the statistics programme (docs/features/materialized-project-statistics/).
7.1 Current state¶
| Component | State | Evidence | Gaps |
|---|---|---|---|
| Foundation: 10 metric families, 7 scope kinds, control rows, guards, fences, receipts, outbox, checkpoints | Merged, dark | ProjectStatisticsMetricFamily.cs:10-41; ProjectStatisticsScopeKind.cs:11-45 (per MS) |
StageQuestionVersion scope reserved, never written; ProjectQuestion is question-only grain |
| Transactional write path | Merged; measured FAIL (all eight cells over the 10% p95 gate, 46%–1,283%; 25 commands per save) | STATUS.md:364 (verified) |
Not viable for production; still the active path wherever writes are on and fold is off |
| Async point fold, slices 0–7 | Merged (slice 7 docs #3962 at 06:13 UTC; #3956 at 05:53 UTC) | gh pr view 3962 3956 (MERGED) |
STATUS still lists slice 7 "In review" (STATUS.md:386) |
| Fold protocol and N-1 window | Protocol 4 = production baseline P0; stamp advance built; StampAdvanceAllowed unset |
ProjectStatisticsFoldProtocol.cs (per MS); rules file |
Each breaking bump needs reset and backfill per project |
| Gate (b) | Provisional fail on a loaded host (14 of 16 cells miss latency; zero statistics-caused conflicts or failures); idle-host rerun on Bramble pending | STATUS.md:393-404 (verified) |
Likely lever is the pre-write durable-mode re-read (design owner's call) |
| Production | No statistics keys in production values; fold enable refused in code for syrftest and unknown databases |
cluster-gitops ea972fd6; ProjectStatisticsFoldAdministration.cs:71, 241 (verified) |
Lifting it is "a separately approved production rollout, never a feature PR" |
| Staging (new since the review) | Writes, Serving, ProjectScreening on; allowlist project …0102. materializedProjectStatisticsFold: true now pinned on API and PM ("staging pilot only (Chris, 2026-10-01) … a project still needs an explicit admin enable"); SYRF__ProjectStatistics__Fold__AllowPartialCoverage: "true" on the API; a statistics operator client (#3908) in staging Identity |
cluster-gitops ea972fd6 (staging/api/values.yaml, staging/project-management/values.yaml, staging/identity/values.yaml) |
STATUS on main still says "the fold flag is off in every environment … no project has fold mode on" (STATUS.md:372-374). Whether project 0102 is in fold mode, and whether the pending index is built in staging, is UNVERIFIED (no database read) |
| Preview | No statistics keys | preview/preview.values.yaml |
Preview pilots have no FEAT-024 at all |
| Per-project admission | Static allowlist on both hosts; durable eligibility #3524 planned, not built | ProjectStatisticsAllowlist.cs:16-30 (per MS); #3524 OPEN |
Eligibility must take part in source admission inside the transaction |
| Soak | Open: #3510 and #3952 (isolated e2e stack on Bramble, per the 30 September decision) | gh issue view (OPEN, updated 3 October) |
Not started |
| Correctness follow-ups | #3840 OPEN again (flag coupling unfixed in code); #3960 (drift re-persist); #3845 (parity calculators); #3849; #3846; #3953; #3910 | gh issue view (all OPEN) |
#3840 blocks QuestionAnswers/ReviewerAnnotation unless both annotation flags are on; #3960 blocks Membership/ReviewerScreening in production |
| Version constants | Backfill families inherit ProjectScreening's version constants | #3506 OPEN | Must be fixed before new families |
| Fixed two | AnnotationThresholds.MinimumNumberSessions = 2 is an input of the shared configuration digest (annotation.v1:mns=); also StudyStats.cs:353-355 and StudyRepository.GetSessionFilter (StudyRepository.cs:1665, 1712, 1725, #3979) |
Verified at all three sites | A target-aware change is a digest-format change |
| Open FEAT-024 PRs | None | gh pr list (15:25 BST) |
— |
7.2 Integration with the plan¶
X-STATS-b is a chain, and Q-31(b) is the designed first pilot path (MS-01, Corrected). "FEAT-024
production readiness for the usage family (gate (b))" names one gate for a chain of seven. By the
plan's own windows (W3: "R2c ships (production per Q-31)") the Q-31(b) path, authoritative counting
and identity enumeration at the protected boundary, will be the first production path, so it is
designed as the first pilot path, not an exception; the materialised family is a swap-in behind
the same interface. This applies Chris's Q-31 decision (OWNER: materialised family as the target,
(b) for named pilots if gate (b) is not reached when R2c is otherwise ready); it changes the
planning expectation, not the decision.
| Step | Prerequisite | Owner | Evidence |
|---|---|---|---|
| X-STATS-b1 | Gate (b) passes on an idle host (Bramble rerun) | FEAT-024 owner | Benchmark record |
| X-STATS-b2 | Soak #3510 via #3952, with #3840, #3960 and #3845 closed for the families in use | FEAT-024 owner | Soak evidence |
| X-STATS-b3 | Production pending index built in an approved window (operator route) | FEAT-024 owner; operator | Runbook record |
| X-STATS-b4 | Production rollout separately approved, lifting the in-code refusal; D1-03 decided "activate" (3 October; the date is still to be set) | Chris | Approval record |
| X-STATS-b5 | Production per-project eligibility: #3524 built after C16 freezes, or the static allowlist on both hosts for named pilots | FEAT-024 owner with L0 | Configuration record |
| X-STATS-b6 | Usage family built (E72): new families, scope kinds, onboarding contract, #3506 | FEAT-024 owner with L7 | Merged code and parity calculator |
| X-STATS-b7 | Usage family proven on staging under X-STATS-a (parity, fence and rebuild fixtures, C8-T07, AC-R2c-24, AC-R2b-13 and AC-R2c-25) | FEAT-024 owner with L7 | Staging evidence |
X-STATS-a (MS-10, Adopted; D3-10b, D3-10d). For an R2c staging pilot to satisfy PS1, the usage
family must be on in staging and the pilot projects allowlisted on both hosts: a FEAT-024 decision,
because STATUS keeps every non-screening family off there. Preview carries no FEAT-024 keys, so
preview pilots are exempt from PS1 and use the (b) path (D3-10d). Project 0102 is both FEAT-024's
only staging pilot and a named plan pilot seed; with the fold flag now pinned on in staging it may
be in fold mode (UNVERIFIED). It stays out of R2a–R3a pilots until the "canonical commit on an
allowlisted project with statistics on" fixture (C8-T07) passes in the e2e stack, then becomes
the deliberate X-STATS-a integration pilot (D3-10b).
Source-write seam (MS-06, Corrected). The engine never writes statistics or pending entries.
It writes Study only through a FEAT-024-owned source-write seam with a projection-only shape (input:
before and after CanonicalSummary plus the affected families; output: nothing, a classified delta,
or an entry/intent, per the project's active path). Correctness floor on every path: every family a
commit can affect is at least staled when statistics are on. Mechanics:
consistency model. This keeps R2a off a protocol that may still change if gate
(b) fails.
New families and scope kinds by technical-plan amendment (MS-02, Corrected). The plan's "no
new counters" and "new kinds inside the existing family" are replaced. The usage family's authority
is pmFormSession and the head collection, not pmStudy; every FEAT-024 family today is rebuilt
from Project and Study and bound to Study.Version. So: new families (FormVersionUsage,
QuestionVersionAnswers; later ProfileVersionDecisions) with new scope kinds and key components
(FormId, FormVersionId, ProfileId, ProfileVersionId), authoritative over the canonical collections in
the same pinned snapshot, delivered by a FEAT-024 technical-plan amendment ("canonical sources") at F2
(forms) and F5 (profiles). Their catalogue and source versions are independent of ProjectScreening's
constants, so #3506 is fixed first (E72).
Drafts counted authoritatively (MS-03, Corrected). A draft-only session never writes Study, so
no FEAT-024 path can point-maintain a draft_only count. The usage family covers explicit versions
only (completed and saved-incomplete, by form version, deduplicated across stages). draft_only is
counted authoritatively from the draft collection (indexed by base form version, E21) inside the
publish fence; the publication manifest records both counts with their basis (AC-R2c-25).
"Current" at the fence and a scoped rebuild API (MS-04, Corrected; D3-10a). No in-process,
project-admin-callable "targeted refresh" exists: republishing a Stale scope is the admin backfill
route (operator identity, synchronous, may answer 409) or the hourly default-off repair. FEAT-024's
reader already answers a Stale scope with a pinned authoritative value in the same snapshot, which
is what PS2 permits ("actively update the specific relevant statistics there and then"). So:
(i) FEAT-024 exposes a service API "scoped rebuild at a pinned snapshot" (reusing
ProjectStatisticsRebuildService.PublishAsync, the sanctioned retry unit) callable by the
publication command under the fence; (ii) recommended (D3-10a): "current" for PS2/PS3 is a FEAT-024
read at the fence whose result is Materialised-Fresh or pinned-Authoritative, with the read's
identity (projection revision, source revision, digest) recorded in the frozen manifest; (iii) the
reviewer "pause" is the engine's scoped write fence, not a FEAT-024 facility (FEAT-024's
definition-rewrite fence makes reads answer 503 for the whole project). A-08 is reworded
(open questions A-08).
Receipts (MS-05, Corrected). FEAT-024 receipts are written only when statistics flags and the
allowlist admit the project (so none in production today), bind Study.Version, and on the fold
path carry the fold time and are missing for overflowed saves. Canonical idempotency cannot depend on
them. E35 is replaced by the canonical command ledger (brief §1.4, owned by the
consistency model); FEAT-024's source-operation receipt stays the
statistics-protocol receipt and reuses the canonical CommandId as its OperationId, with the same
digest, so one command has at most one statistics operation.
Target-aware classification (MS-07, AP-14, Corrected; X-STATS-c). "Enough = 2" lives at three
sites (verified): StudyStats.cs:353-355 (a TODO since #2331), FEAT-024's
AnnotationThresholds.MinimumNumberSessions (digest input annotation.v1:mns=;
ProjectStatisticsConfigurationDigest.cs:16), and the study-library session filter
(StudyRepository.cs:1665, 1712, 1725, #3979). Replacing it is a catalogue version bump for the
stage, membership-stage, domain-reconciliation and reviewer annotation families and a digest-format
change, which the rules say needs "an explicit compatibility/reconciliation plan for established
controls, current rows and retained checkpoints before serving is re-enabled". It is a named
FEAT-024-owned change with its own issue (E71), landed before R2b pilots on allowlisted projects
and before R3a; AC-R3a-06 references it. Until then AC-R3a-06 cannot pass for materialised consumers.
Profile grain (MS-08, D3-10c). ProjectScreening, MembershipScreening and ReviewerScreening read
legacy ScreeningInfo (one decision per reviewer per project). R3a's single default profile can be
projected into that shape exactly: R3a states that the Study projection writes the default profile's
decision into legacy ScreeningInfo/InclusionInfo, so the three families stay exact, verified by
the parity audit (AC-R3a-34). R3b's several profiles cannot. Recommended (D3-10c):
multi-profile screening statistics are served live for R3b pilots; profile-grain families
(ProjectProfile and MembershipProfile scope kinds) arrive at F5 as a FEAT-024 scope amendment.
Three N-1 mechanisms (MS-09, Corrected). C8 and C16 name them separately: (a) BSON
extra-element tolerance for value-object maps (#3145, #3512), which R0 needs; (b) the fold protocol
window (protocol bumps, stamp advance), which governs pending-entry derivation only; © per-family
catalogue and source versions plus the configuration digest, which govern rows, guards and
checkpoints (coupled to ProjectScreening's constants until #3506). R0's floor is (a); new transition
kinds for canonical commits are (b); new families, dimensions and target-aware classification are
©.
PRISMA never from FEAT-024 rows (MS-11, Corrected). A source-type dimension is new
SearchPopulation metric keys (a catalogue and source-version change); retained checkpoints never
gain it; SearchPopulation counts SystematicSearch.NumberOfStudies and has no withdrawal notion.
PRISMA snapshots are computed from authoritative records (Citations, external step records,
ScreeningOutcomes) at the report watermark and stored frozen. AC-P1-07 is reworded
(acceptance criteria §4.23).
Per-project commit sequence rejected (MS-12, Corrected). A per-project counter written in every
canonical commit serialises a project's commits on one document; FEAT-024 measured exactly that
pattern with the Project token. Brief §1.2 deletes ProjectCommitSequence from interactive commits
(per-aggregate versions plus HLC; consistency model). The programme side is
R10: a canonical-commit arm in FEAT-024's write benchmark (½/5/10 reviewers, same and different
Study, eligibility off and on), which also measures D1-08's gate.
Onboarding contract and protocol 5 (MS-14, MS-15, Adopted). Appending enum ordinals is not
enough for rolling deploys: ProjectStatisticsScopeKey.Compose throws on an unknown scope kind, and
the daily producer, fleet dispatch, repair, drift check and admin router enumerate families. FEAT-024
publishes a new-family onboarding contract with a rolling-deploy test before F2 (R3). Transition
kinds for canonical commits (form-keyed session versions, form-keyed claims, profile-keyed decisions)
go into one additive bump, protocol 5, declared at F3 and shipped dark only after gate (b); until
then canonical commits carry invalidation intents only (allowed by the N-1 rules; exact enough for
pilots, which are served live for those families). No bump sits on R2a's critical path (R6).
Rollback order and adoption fences (MS-16, MS-17, Adopted). When a release changed a statistics
writer, family or protocol, the rollback rehearsal (AC-ALL-04) follows FEAT-024's order: fold-disable
and wait for Disabled; close the project gate (two-stage with quarantine); close the fleet gate;
flags off through GitOps on both hosts; allowlist guard; then images; a rollback past an advanced
stamp needs reset twice around guard removal; record fold mode and stamp before and after.
Adoption shadow and cutover raise FEAT-024's staged operation fence for every family (as bulk update
does) and reset and rebuild under the new family source version after cutover. Per-family parity
audits (#3845) are a G-ADOPT prerequisite, or AC-R6-04 labels statistics parity as manual for the
families without one.
QuestionAnswers and the canonical designer (MS-18, Adopted). For canonical projects the
designer reads QuestionVersionAnswers (or authoritative counts), never the legacy locks; FEAT-024's
QuestionExists resolves canonical definitions for canonical projects.
Agreement in its own store (MS-20, D3-11). Recommended: R5c keeps a rebuildable derived store keyed by (project, form version, method version) with a watermark and the independent/informed split, computed by a bounded background job; never a FEAT-024 family; AC-R5c gains an absolute budget. FEAT-024's exclusion of kappa stands.
Overview DTOs (MS-21, Adopted). New gate, sufficiency, work-status and batch-frontier fields
extend the existing statistics query services and consumer flags (StageOverviewStatisticsQuery,
ProjectReviewerProgressQuery, ReviewerProgressQuery), one read per route; no parallel overview
endpoint (C17).
History is never an as-of input (MS-22, Adopted). FEAT-024 history is daily observed counters
that are never reconstructed; C11 and C12 gain the rule; for canonical projects the daily
observation records the new families under their captured catalogue version.
#3524 after C16 (MS-24, Adopted). R0's admission record is never the statistics allowlist.
Durable eligibility (#3524) stays FEAT-024-owned and is built after C16 freezes, so both records share one admission-service
shape and audit, with statistics eligibility read inside the transaction by FEAT-024's own gates
(R8).
No transactional point mode for admitted projects (DC-21, Corrected). In transactional
(non-fold) point mode every canonical transaction would also write six per-project statistics
documents and the Project token (screening-write-overhead-diagnosis.md:128-141, per DC). Admitted
projects run FEAT-024 in fold mode or with statistics writes off; R0's admission service refuses the
combination "admitted and allowlisted with writes on but not in fold mode".
Activate or freeze (#3987, D1-03). The architecture review asks whether ProjectStatistics (about 56% of PM Core, 36 collections, about 25 flags) is activated on a date or frozen. Decided (D1-03, Chris, 3 October, as recommended): activate the families this plan uses, on a date still to be set; G0 needs that date. A freeze, which was not chosen, would have made Q-31(b) the GA path for publication evidence (authoritative counting at the protected boundary), shrunk E72 to the authoritative counting service, dropped X-STATS-b and served statistics in R2–R5 live. So Q-31(b) is not extended to GA, and X-STATS-b1 to b7 stay on the GA path.
7.3 Recommended changes to the existing implementation¶
| # | Change | Rationale | Timing | Compatibility and migration | Risk | PR slicing | Owner |
|---|---|---|---|---|---|---|---|
| R1 | Extract a single source-write participant seam from the screening and annotation statistics writers and fold saves, with a projection-only Study write shape | MS-06 | Before F1a (the engine builds against it) | Pure refactor; command-budget tests stay byte-identical | Low | Refactor PR; projection-only shape with intents | FEAT-024 owner |
| R2 | Target-aware annotation classification at all three sites; catalogue bump (annotation.v2); mns moved out of the digest into per-stage definition metadata (E71) |
MS-07, AP-14, #3979 | Before R2b pilots on allowlisted projects and R3a; after the gate (b) rerun so the benchmark baseline is stable | Digest-format change with a reconciliation plan (forced rebuild of allowlisted projects; retained checkpoints keep identity); ProjectStageConfigurationChange.AffectedFamilies extended |
Medium (staging pilot reset and backfill; history identity) | PR a: calculator, classifier, parity tests. PR b: digest and version migration with runbook step. #3979 separately (study-library filter) | FEAT-024 owner; architecture-review owner for #3979 |
| R3 | New-family onboarding contract and rolling-deploy test; fix #3506 | MS-14, MS-09© | Before F2 | Additive; tests, plus the constant split | Low | One PR | FEAT-024 owner |
| R4 | Scope kinds and key components: form version and question version at F2; profile version, project profile, membership profile at F5 | MS-02, MS-08 | F2, F5 | Class maps already ignore extra elements; append-only ordinals | Low | One PR per freeze | FEAT-024 owner with L2/L3 |
| R5 | Usage families over canonical collections in the same pinned snapshot, with backfill and rebuild routes, parity calculator, flag, consumer manifest, fleet dispatch, repair and drift inclusion, and the scoped-rebuild-at-pinned-snapshot service API (E72) | MS-02, MS-03, MS-04 | After F2; dark; staging enable for pilots under X-STATS-a | New families under their own catalogue and source versions; explicit versions only | Medium | 3–4 PRs (amendment and ADR; family and routes; parity and fleet; service API) | FEAT-024 owner with L7 |
| R6 | Protocol 5: every canonical transition kind in one additive bump with IntroducedAt = 5, N-1 harness cases and the stamp-advance procedure |
MS-15 | After F3 and only after gate (b); until then intents only | Additive from P0; stamp advance after both rollouts | Medium | One PR per kind group, one bump | FEAT-024 owner |
| R7 | Close the production-blocking follow-ups: #3840, #3960, #3845, the gate (b) rerun, the soak | MS-01 | Now (already open) | None | Low to medium | Existing issues | FEAT-024 owner |
| R8 | Durable eligibility #3524 after C16 freezes, aligned to the admission record shape and audit, keeping in-transaction participation | MS-24 | After F1a | Explicit migration from the static allowlist (no silent union) | Medium | 2 PRs | FEAT-024 owner with L0 |
| R9 | Hand fold deferred item 2 (eligibility Project token) to the StageSettings design | MS-19, DC-05 | F3 | Eligibility-programme change behind its flag | Medium | One PR (L4) | L4 with eligibility owner |
| R10 | Canonical-commit arm in ProjectScreeningWriteBenchmark |
MS-12, D1-08 | M0/F1a | None | Low | One PR | L1 using FEAT-024's harness |
| R11 | Correct STATUS: slice 7 merged; #3591, #3766, #3767 merged; staging fold flag pinned on | MS-23 and the refresh | Before G0 | None | Low | One docs PR | FEAT-024 owner |
7.4 Read-model placement¶
| Read model | Placement | Why | Release |
|---|---|---|---|
| Form-version usage, question-version answers | New FEAT-024 families (E72) | Bounded, catalogue-defined, parity-auditable, needed on the publication page | F2 / R2c (served authoritatively under Q-31(b) until X-STATS-b) |
| Profile-version decisions; profile-grain screening | FEAT-024 scope amendment at F5, or served live (D3-10c) | Legacy screening families cannot represent several profiles | F5 / R3b |
| Task-based domain reconciliation counts | FEAT-024 family change at R4a | Today's DomainReconciliation counts two booleans per stage | R4a |
| Existing screening and annotation families on canonical projects | Kept exact through the Study projection (default profile written into legacy ScreeningInfo; tallies merged) |
Avoids spending R3a on R3b's timeline | R2a–R3a |
| Gate status, sufficiency, batch frontier | Derived summaries, not persisted, extending existing statistics queries | Configuration-dependent, cheap to derive, one read per route | R3a–R3c |
| Draft-only counts | Authoritative indexed count on the draft collection, inside the fence | No Study write exists to maintain them | R2c |
| Queues (my concerns, assigned work, requested reviews, changes awaiting approval) | Authoritative indexed counts over the feature aggregates, computed at read time | Group routing must reach people granted after an event | R3c–R4b |
| Publication impact manifests | Authoritative enumeration of identities in the fenced snapshot | Counts never decide identities | R2c |
| Agreement | Separate rebuildable derived store with a watermark (D3-11) | FEAT-024 excludes kappa; heavy, method-versioned | R5c |
| PRISMA | Frozen snapshots from authoritative records at the report watermark | Reports must regenerate identically | R5b |
| Allocation progress | Allocation keeps reading SessionTally through the Study projection; per-reviewer counters only as decided at F1a (§3.4) |
No third counter store | AL1 |
7.5 Do not build further until¶
Point maintenance for QuestionAnswers at question grain (fold deferred item 6): until F2 fixes the
question-version grain. Any writer to the reserved StageQuestionVersion scope. Profile grain in the
screening families: until F5. #3524: until C16 freezes. Any further per-release fold-protocol bump
planning: until gate (b) is decided.
7.6 What the plan adopts¶
"Hot paths never write statistics" and the whole-pending-set rule as engine rules; the pinned command-budget tests as the model for the canonical commit's budget tests; the rebuild publication as the sanctioned retry unit; fail-closed quarantine of unknown entry kinds; the transaction-admission pattern (snapshot read, captured flags, unknown-commit-result retry) as input to C18; the benchmark harness and its cells for D1-08; the soak on the isolated e2e stack, never on staging with real accounts.
8. Notification service¶
Owned by the notification programme (the "SyRF Notifications and Study Issues" conversation, delegated to the Juniper thread; plan lane L14 only specifies contracts).
8.1 Current state¶
| PR | State at 15:18 BST (#3965 at 20:35 BST) | Head | Notes |
|---|---|---|---|
| #3932 inbox and export capture | OPEN, CONFLICTING; base main |
2ddd96fb0; 51 files +2,257/−188 |
Conflicts only in generated files (per NS); no human approval; inbox reads gated by the capture flag |
| #3938 import and invitation | OPEN, mergeable into #3932's branch | 180f0d1bb; 22 files +693/−23 |
Opted-in users get duplicate invitation email (NS-25); #3967 since changed invitation-token matching on main |
| #3941 access and workload | OPEN, mergeable in stack | eb72c886d; 26 files +915/−67 |
Random SourceIds; legacy ACL and stage targets; wraps ProjectController.UpdateProject, which #3964 also edits |
| #3942 preferences and immediate email | OPEN, mergeable in stack | 75f89b33d; 48 files +1,877/−109 |
Exact-set category validation; preferences unflagged; no unsubscribe, no halt |
| #3943 daily digests | OPEN, mergeable in stack | d75fd81b9; 23 files +1,010/−30 |
Titles only, ungrouped, at most 100 a day |
| #3944 reconciliation conversations | OPEN, mergeable in stack | 59ab32e7b; 52 files +2,222/−24 |
Reads legacy sessions; keyed by stage |
| #3945 study issues | OPEN, mergeable in stack | 25b364da3; 49 files +2,256/−28 |
Writes Study bibliographic fields (version-guarded, lock-aware; verified StudyIssueRepository.cs:69-87); recipients are Administrator-group members |
| #3947 checked PDFs | OPEN, mergeable in stack | ae3c6749d; 40 files +3,009/−16 |
Writes PdfRelativePath (verified CheckedPdfProposalRepository.cs:125-139); adds native tools to the API image; needs a clamd scanner and an isolation review |
| #3965 private conversations | OPEN, ready for review (20:35 BST): ten commits pushed, base = #3947's branch (codex/checked-pdf-proposals-and-explicit-administrator-a-r6bdl2); waits for the stack (D1-09). Contents: reconciliationConversations flag depending on notificationInbox; one-to-one threads; completed non-reconciliation sessions only; read-only context; a fix round (all-or-nothing thread creation, a content-free "questioned" marker for every reconciler, stable reviewer labels, typed reply refusals; docs); and a tenth commit refusing a reconciler who reviewed the study in any stage. (At 15:18 BST GitHub showed a draft with 0 files; at 19:25 BST eight commits were local.) |
986b1cdc2 |
Completed-only candidacy already excludes RA5 requested reviewers until they return their review (answers NS's Q-N9; recorded in the register, no question). The reconciler self-review refusal is now stage-independent: any non-reconciliation annotation session of the reconciler on the study, in any stage and any status, refuses them (stricter than NS-05's legacy rule, which needs overlapping questions; commit 986b1cdc2 message). Threads are still keyed by stage and read ExtractionInfo.Sessions |
| Checks and reviews | Stack E2E jobs skipped (no run:e2e-* label); latest-head Claude reviews were short delta reviews; product CI runs only while a slice is temporarily retargeted to main |
— | Per NS §2 and the stack's technical-plan.md "CI validation for stacked slices" |
| Flags and workers | notificationInbox, notificationEmail, studyAttention (requires inbox), environment-wide, API and web only; change-stream watcher, email worker and digest worker start in every API replica regardless of flags; accepted email continues after the flag goes off |
— | Per NS; the stack plan says durable workers "must finish accepted obligations even if admission/UI is disabled" (technical-plan.md:64-65) |
| Follow-up tracker | #3950 OPEN (13 items) | — | Covers only an observer-load item among those the plan assumed (NS-22) |
8.2 Integration with the plan¶
C15 v2 (NS-01, NS-03, NS-12, Corrected/Adopted). C15 required capture "in the same transaction
as the domain change" and the notifications page forbade any outbox. Several of the plan's own events
have no such transaction (publication phase 2, outdated-flag fan-out, time-driven expiry and LC1
reminders, adoption notices), and the stack's extension model (one kind = one email category, a
central resolver switch, a typed field per source type, five inbox writers) was built for eight fixed
kinds while the plan needs about twenty more. C15 v2 is frozen at F1b (contract) with kinds per
feature; its full text is C15 in contracts. Summary:
- A kind registry (kind, email category, scope legacy/canonical/both, admission flag, inline recipient limit, resolver), registered through DI by the owning feature; a registry test fails the build when a kind lacks a category, resolver, label or disclosure fixtures.
- A generic
Sourcesub-document; one capture service for all writers; server-provided action label, context lines, typed availability and workflow state. - Two capture modes: inline (active source transaction, bounded recipients) and recorded
fan-out (a durable intent,
NotificationFanOut, in the source transaction; a leased idempotent expander writes rows in batches). Time-driven notices are domain transitions run by a scheduler that marks the aggregate and captures inline. This is C19 class (b) for notifications; "no outbox" becomes "no second notification store; durable intents are part of C15; in-memory outboxes stay forbidden" (the stack's own principle). - Deterministic identity: SourceId = SHA-256(kind, source type, source ID, occurrence key); row ID =
SHA-256(SourceId, recipient); occurrence key from durable identity (command ID, aggregate version,
transition, publish operation ID); upsert with
$setOnInsert, neverInsertOne. "AsreviewAccessGrantedalready does" is deleted: that kind usesGuid.NewGuid(). - A disclosure-policy hook per channel (inbox, email, digest) after every resolver (NS §4.2).
Preferences tolerance before #3942 merges (NS-02, Adopted). Exact-set validation makes every new
kind a breaking change: in a rolling deploy or rollback, whichever side has a different category list
rejects every preference save with 400, including opt-out, and the preferences API is unflagged.
Before #3942 merges: unknown keys kept and ignored, missing keys mean off, the client sends back keys
it does not render, a kind→email-category map (the eight existing kinds map to themselves; the
review-workflow kinds map to at most six new categories,
notifications integration §3), and a version-skew test
matrix (C15-T06). If #3942 merges unchanged, the tolerant validator must still deploy one release
before the first new category.
Enablement controls and G-NOTIF (NS-04, Adopted; D3-21). Today enablement cannot be scoped or
fully reversed: flags are environment-wide; turning email off does not pause accepted mail; turning
the inbox off 404s links already emailed; the email flag declares no dependency on the inbox; staging
and preview flags can be overridden at runtime by any SyRF administrator. Before any enablement
outside the e2e stack and Mailpit: per-project notification admission (an R0 enrolment scope, or an
interim registry until R0 exists) checked at capture for every kind; an operator delivery halt that
pauses without cancelling; inbox reads available whenever saved items exist; the declared
email→inbox dependency (E74). G-NOTIF is a gate per environment and kind family that Chris
approves, recorded in the flag audit comment (D3-21: pilot projects first, platform-wide after pilot
exit; staging and preview overrides only with his approval per release, email kept in Mailpit).
Because runtime overrides are currently reset on resolution (#3975), G-NOTIF evidence never relies on
a runtime override (X-ARCH-c).
Merge order and #3965 (NS-05, NS-17; D1-09). Order approved by Chris on 3 October, as recommended:
- The ownership fix, #3964 (D1-01 decided; #3969's active-member check and tests ported). It edits
ProjectController.UpdateProject, which #3941 wraps. Done: merged at 20:27 BST on 3 October (85e6facf7). -
3932: regenerate the generated files on current
main; full checks on the head retargeted to¶main; arun:e2e-fullrun; human approval. -
3938.¶
-
3941: rebased on step 0, with a test that a refused owner-reserved grant captures nothing.¶
-
3942, with the tolerant preferences.¶
-
3943.¶
-
3944, then #3965 immediately after (restacked onto #3944).¶
-
3945.¶
-
3947 last (native PDF tools isolation, or moved out of the API image).¶
Restacking #3965 is the stack owner's call (D1-09): if declined, #3965 stays on #3947 and lands in
the same merge train before any environment enables conversations. X-NOTIF is met when steps 1–5
are merged with flags off; R1c's access notices also need step 3; R4a's conversation work needs step
6. #3965's ten pushed commits (head 986b1cdc2) implement the Q-10 list (own flag with declared
dependency; one-to-one; completed sessions only; a content-free "questioned" marker; read-only link;
stable labels) and, since the tenth commit, a cross-stage refusal for legacy scopes: a reconciler
with any non-reconciliation annotation session on the study, in any stage, is refused (stricter than
the overlap-only legacy rule proposed here). From R4a the rule becomes: refuse any session on that
study × form. That rule is an R4a precondition at the latest (AC-R4a-21).
Exposure (NS-06, Adopted). C3 gains the exposure kind "questioned in reconciliation" (session,
thread, time); every later version of that reviewer's session on that study × form is informed; R5c,
C11 manifests and R6 mapping consume it; #3965 stores the record so it can be looked up by session.
Fixture: a questioned reviewer saves a new version and agreement classifies it as informed
(AC-R5c-07).
Queues surfaced without notifications (NS-07, Adopted; D3-07). A passive per-project queue does
not "alert the project admin" (LC1) or let a raiser "receive" an outcome (QY6) for someone who never
opens that project. C17 gains a flag-independent surface: a global navigation badge with counts
computed at read time over the four queues; a cross-project "My work" tab beside "Pending Projects";
a project-overview banner for admins with pending LC1 requests (placement per D3-07 and the
UX strategy). Acceptance: with every notification flag off, a user reaches an
assignment in an unopened project from the project index in one step (AC-R3c-11, AC-R4a-47).
Study writers in the stack (NS-08, Corrected). Study-issue acceptance rewrites Title, Abstract,
Year, DOI or Url, and checked-PDF approval rewrites the PDF path (both verified, CODE-PR). Both join
the M0 inventory (AC-M0-03). For admitted projects after P2, an accepted bibliographic correction
appends a correction event, re-runs DOI/PMID matching and never edits Citations; a PDF approval
records a P1 retrieval event. Owner: L12 with the study-attention owner.
#3941 alignment (NS-09, Corrected). #3941 decides effective Review access through the legacy ACL
path, ignores Reconcile grants, and computes workload from stage.WorkloadShares and
stage.SessionCountTarget. Changes: read effective grants through the active decision path with a
parity test, and add Reconcile grants; for canonical forms derive workload notices from the form
target and the AL1 plan, and suppress the stage-based capture. AC-R1c-04 becomes conditional on
X-NOTIF (step 3) with a parity test; dashed X-NOTIF edges go to R1c, R2c, R3c, R4a and R4b.
UI1 for stack screens (NS-10, D3-01). "Retain the stack's UI" becomes: the inbox and preferences pass UI-1 to UI-8 before testers see them, and the conversation, issue and PDF screens before production; a Material 3 inbox redesign PR lands before R2c (D3-01).
Flood controls (NS-13, Adopted). Before R2c: mark-all-read; project and kind filters; a context
line from the resolver; "hide unavailable"; an unread count that excludes resolved items (D3-23);
digests grouped by project and kind; jittered refetch and per-request authority memoisation (#3950
item 1); a recipient threshold for recorded fan-out (PROPOSAL: 200); older publication notices for
the same form marked superseded.
Q-20 narrowed (NS-14, PROPOSAL). Candidate answers belong to their reviewer, so outdated flags
on a session are normally its owner's own doing, and the ledger already requires an in-form alert.
No notice for self-caused flags; otherwise one in-app notice per recipient per cause when the cause is
a support on-behalf-of write, an adoption remap or a shared-gold revision
(open questions Q-20).
A publication writes no revision under D2-01, so it never causes this flag; its effects reach
reviewers through the "form version published" notice.
Catalogue rows and taxonomy (NS-15, Adopted). Added: a phase-2 completed or stalled notice for
the publishing admin; canonical workload changes; R6 cutover, pilot admission, removal and read-only
(U28); a kind for Reconcile grants. Publication is one kind whose per-recipient effect is worked out
at read time (two kinds would breach AC-R2c-07's "at most one notice"). Live banners use the existing
per-user SignalR groups, not C15. Taxonomy in
notifications integration §3.
Live on merge (NS-16, Adopted). Merging changes production even with flags off: unflagged
preference endpoints, five web routes guarded only by sign-in, isolated reads and a join-response fix
on project endpoints, three hosted services in every replica, native PDF tools in the API image. The
owner is asked to add route flag guards (keeping opt-out reachable while the account has pending
mail) and to make the workers idle when no flag is on and nothing is pending (E74).
Ownership transfers (NS-19, Adopted). The notification programme keeps capture, inbox, email
and digests. StudyConversation moves to L6 at R4a (rebound to ReconciliationTask; until then
conversations refuse canonical scopes through R0's ownership record, NS-18). Study issues and checked
PDFs move to the Study Management and PDF programmes. #3947 is listed once, under the PDF programme.
Capabilities (NS-20, Adopted). C10's catalogue gains "receive and resolve study issues" and
"approve PDF corrections" at F1b; recipients expand by capability before R1c ships (today they are
Administrator-group members).
Retention (NS-21, Adopted). E32 covers preferences, the delivery ledger, digests (which hold
notification IDs), conversation and issue posts (free text) and PDF bytes, not just inbox items;
retention never orphans ledger or digest references; post text follows the annotation erasure rule.
#3950 coverage (NS-22, Corrected). #3950 does not track retention, fan-out limits, unsubscribe,
the email flag dependency, a halt or per-project admission. These are marked untracked; the owner is
asked to open issues (no GitHub write here).
Issues versus queries (NS-24, Adopted). After R4b, the issue form in canonical projects sends
answer disputes to "Raise a query"; C9 says study issues never carry answer concerns.
Duplicate invitation email (NS-25, Follow-up). Remove projectInvitation from the email
categories, or skip notification mail when invitation mail was sent: a stack backlog item.
Unknown-kind rollback (NS-26, Adopted). Older binaries mark an unknown kind's ledger row
Suppressed (terminal). "An unknown kind leaves the ledger row ready" ships one deploy before the
first new kind; the rollback section notes it.
Other. #3944 is removed from the Study projection's readers; conversations refuse canonical
scopes until R4a (NS-18, Corrected). Each operation in the transaction table gets a capture mode:
Save/Complete inline and bounded (task holder, requesting reconciler); gold publication and query
resolution inline to raisers; publication recorded fan-out keyed by the publish operation; benchmark
with capture on (NS-23, consistency model). C15-T01 to T11 apply to every
release that adds a kind (NS-11).
8.3 Recommended changes to the existing implementation¶
Numbering follows NS §4.
| # | Change | Rationale | Timing | Compatibility and migration | Risk | PR slicing | Owner |
|---|---|---|---|---|---|---|---|
| N1 | Tolerant preference validation; client sends back unknown keys; kind→category map; declare notificationEmail → notificationInbox (E74) |
NS-02, NS-04 | Before #3942 merges (at the latest one deploy before the first new category) | None: existing kinds map to themselves | Low; invalid modes and time zones still rejected | Amend #3942 | Notification programme |
| N2 | Restack #3965 onto #3944; review it (pushed and ready, ten commits; the legacy cross-stage reconciler refusal is in the tenth) | NS-05, NS-06; D1-09 | Before #3945 merges and before studyAttention or reconciliationConversations is enabled anywhere |
Generated flag files regenerate; #3945 and #3947 rebase mechanically; no data yet | Low to medium | Retarget #3965; merge straight after #3944 | Notification programme; L6 reviews |
| N3 | Delivery halt (pause, not cancel); inbox reads independent of capture; per-project notification admission (E74) | NS-04, D3-21 | Halt and read gate before any enablement with real users; admission before any production pilot with notices | New data with safe defaults | Medium | N3a: halt and read gate. N3b: interim admission registry, later an R0 enrolment scope | Notification programme with L0 |
| N4 | Kind registry, Source sub-document, single capture service, server labels, unknown-kind behaviour (E73) |
NS-03, NS-12, NS-26 | ADR in W0, frozen at F1b; landed after the base merges and before the first review-workflow kind | Additive fields; existing kinds keep typed fields (no data migration; Entity extra elements keep older binaries tolerant) |
Medium; mitigated by the existing physical tests | N4a: registry with existing kinds as handlers. N4b: capture service (bulk upsert, recipient cap). N4c: client uses server labels | Notification programme; L14 writes the specification |
| N5 | Recorded fan-out: durable intent plus leased expander (E73) | NS-01 | Before R2c and before R1c bulk group edits | New collection; older binaries ignore intents, which wait until roll-forward | Medium | One PR with intent, worker and resume tests | Notification programme; L2 consumes |
| N6 | Disclosure-policy hook with channel rules and one alias source | NS-03, NS-11 | Interface at F1b with C10 (default keeps today's behaviour); BL1 and VS1 rules before R4a and R4b kinds | Code only | Low | Hook and fixture harness | L8 with the notification programme |
| N7 | #3941 alignment: active decision path, Reconcile grants, canonical workload suppression | NS-09 | Grants before R1c; workload before R2b and AL1 | Code; parity tests | Medium | One PR | Notification programme with authorization and allocation owners |
| N8 | Material 3 inbox and preferences; grouped digests; flood controls; resolved and superseded state | NS-10, NS-13; D3-01, D3-23 | Screens before testers see them; flood controls before R2c | Additive ResolvedAtUtc; new endpoints |
Low to medium | N8a: inbox redesign. N8b: grouped digest. N8c: refetch jitter and memoisation | Notification programme; L16 design review |
| N9 | Register the #3945 and #3947 Study writers; P1 and P2 hooks | NS-08 | Inventory at M0; hooks before P1 and P2 | Code only | Low | One PR per hook | L12 with the study-attention owner |
| N10 | Conversations refuse canonical scopes; rebind to the task at R4a; stable aliases; L6 takes ownership | NS-05, NS-18, NS-19 | Refusal before R2a pilots; rebind at F4/R4a | R6 manifests remap references (already planned) | Medium | R0 refusal check; R4a rebind | L0; L6 |
| N11 | Merge #3947 last; move native PDF tools out of the API image (for example into the PDF Agent) or finish the isolation review first | NS-05, NS-16 | Before #3947 merges | Image change only | Medium | Amend #3947 | PDF and study-attention owner |
| N12 | Retention and erasure after the E32 decision; an ADR "Saved notification capture and delivery"; MongoDB reference entries for the new collections | NS-21, NS-17 | E32 at F1a; implementation before production enablement | A TTL index applies to existing rows; the digest processor already tolerates missing items | Low | One PR plus the ADR | Notification programme |
8.4 Do not build further until¶
- New notification kinds beyond the eight existing ones: until C15 v2 is frozen (F1b) and N4 lands.
- Email for any new category outside Mailpit: until N1, N3 and G-NOTIF.
- Conversation features that assume stage keys (handover, open-task-only threads): until R4a rebinds
them to
ReconciliationTask.
8.5 What the plan adopts¶
The saved inbox row as the durable obligation; read state never resolving a workflow; generic
payloads shaped by the resolver under fresh authority; deterministic SHA-256 identities
(StudyConversation, StudyIssue) as the model for every kind; the per-item delivery ledger with
leases and ambiguous-send handling; "durable workers finish accepted obligations"; Mailpit and the
hermetic e2e stack as the only places test mail goes.
9. Authorization programme¶
Owned by the authorization programme (#3335, OPEN; handover plan handover/2026-09-08-authorization-3335/PLAN.md).
Joins (existing, unchanged): X-AUTH-SCHEMA (gate G-D: membership schema 1 applied and verified in production, after WP-M1/WP-M2) and X-AUTH-ENFORCE (M6 staged cutover, gate G-C, or parity tests) for R1c; X-AUTH-WP9 (explanations, after WP3) for R1b's explanations and R1c.
New join X-AUTH-RESOLVER (#3251; AP-02). An out-of-request, cross-provider authority resolver on
the single ProjectAuthorityEvaluator, needed by AL1, allocation reviewer validity and Phase 2, and
R3a's per-reviewer "Who is offered what". Until it lands R3a ships pool-level counts (D3-13d).
Catalogue additions (C10, at F1b): realtime presence as a disclosure channel (RT-14, D3-20); "view who is reviewing" folded into Monitor; "receive and resolve study issues" and "approve PDF corrections" (NS-20); the claim kinds' admin actions (release a task editor claim, override an assignment) mapped to existing grants in E6.
Ownership fix (D1-01, decided). Chris chose #3964: the controller compares the caller with the
persisted owner, so a stored ChangeOwner grant cannot bypass it, and owner-reserved activities are
refused by the permission endpoints with an aggregate backstop. #3969's active-member check and its
tests are ported into #3964, and #3969 is closed by its owner. State at 20:35 BST: #3964 merged at
20:27 BST (merge commit 85e6facf7) after an approving Claude review on head 16788f9 and green
checks, and its worktree and branches were removed; #3969 is closed with a comment pointing to
3964. (At 15:20 BST #3964's three commits were local and #3969 had one pushed WIP commit.) Step 0¶
of the notification merge order (§8.2) is therefore done, and R1b's security-fix entry criterion is met.
Recommended change for the authorization programme: deliver #3251 through the evaluator (A5); schedule WP11's split (dialog over existing groups first) as the plan already proposed; add the catalogue entries above to its coverage test (#3882) when they land.
10. Architecture review programme¶
The architecture review is #3961 (draft docs PR; head 809ae76d7 at the 19:25 BST re-check, updated
18:13 UTC), with issues #3972–#3990, run by another session. Its premise ("the highest-leverage work
is to make the existing rules enforceable and to remove surface area, not to add features") competes
with this plan for the same approver, agents, CI and files, and several items change foundations that
F1a freezes (DS-02, Corrected: the programme was missing from plan §8 and §10). Its synthesis still
lists #3964 versus #3969 as open; D1-01 has since decided it (#3964 merged, #3969 closed).
| Finding (issue) | Why it matters to this plan | Joint sequencing (D1-02, approved 3 October) |
|---|---|---|
Persistence unsound under concurrency: singleton unit of work, shared mutable 2-second cache, version bumped in memory before the write (Audit.cs:28-34, per DS-02), upserts that resurrect deleted documents, direct $set writes without a version bump (#3985) |
Canonical commits rely on version CAS and isolated reads (C18). Verified examples of version-less direct writes on pmStudy: the question-delete cascade and the inclusion recalculation (StudyRepository.cs:1306-1319, 1620-1650; no Audit update in InclusionInfoFiltersAndUpdates) |
X-ARCH-a: #3985 is an F1a prerequisite, or canonical repositories use isolated reads and non-upsert saves enforced by an architecture test (A-34) |
| Fire-and-forget domain events (#3973) | C19's in-process IDomainEvent is allowed only for loss-tolerant effects, and only after events are awaited |
X-ARCH-a: #3973 before F1a |
| Lamar last-registration-wins; runtime flag provider re-created per resolve, resetting overrides (#3975) | Admission and evidence must not depend on runtime overrides; AP-22's double read becomes real | X-ARCH-c: #3975 before R0 relies on runtime overrides and before any G-NOTIF evidence |
| MassTransit v8 open-source support ends December 2026 (#3986); global retry and outbox (#3984) | R0 adds ownership guards to PM consumers; tracking's idle, suspension and liveness consumers and scheduled commands are MassTransit/Quartz paths | X-ARCH-b: the #3986 decision before R0 guards PM consumers; new consumers follow it |
| ProjectStatistics activate or freeze (#3987) | Decides X-STATS-b and E72's size | D1-03 decided 3 October: activate (the date is still to be set; G0 needs it) |
| One v0/v1 schema migration (#3988) | System-question snapshots (E24) depend on SystemQuestionVersion variants |
Fold into E24, or defer until after R2a (D1-02) |
Web state convergence; god files stage-review.component.ts, the AF2 store (#3989) |
L5's serialisation point | Run as L5 seam slices in the L5 order (plan §7) |
| Study library filter hard-codes two reviewers (#3979) | One of the three fixed-two sites | X-ARCH-d: before R3a, alongside E71 |
ValueObject equality ignores base fields; agreement threshold lost in JSON (#3980) |
R3a's compatibility profile reproduces the project threshold | X-ARCH-d: before R3a |
Modular monolith with one writer per collection; job state out of Project |
Canonical collections should have one writer host each; Project is already a hot document |
Adopted for canonical collections: one writer host per collection, declared in C16 |
Frontend bug-fix plan (docs/planning/frontend-bug-fix-plan-2026-10.md in #3961, added 19:12 BST): PR-3 SignalR groups not re-subscribed after automatic reconnect (#3976); PR-7 a bounded wait on AF2 saves; PR-9 stage-review effects (review-effects.ts), pending that plan's decision D2 |
PR-3 touches the client that carries presence and #3932's InboxChanged hint; whether presence rejoin is affected is UNVERIFIED (its store has its own reconnect logic). PR-7 and PR-9 touch R2a's AF2 persistence and the reviewer workspace, L5's serialisation point |
PR-3 before any tracked pilot (checked by AC-T-01) and before G-NOTIF; PR-7 agreed with the AF2 owner before R2a's drafts adapter; PR-9, if done, lands before L5's R2a changes or joins the L5 order |
The plan adds the programme to §8 with these joins and its platform risks to §10. Chris approved D1-02 as recommended on 3 October (register §1.13):
3961 Phase 0 continues; #3985 and #3973 are F1a prerequisites; #3986 decided before R0; #3988¶
folded into E24 or after R2a; #3989 as L5 seam slices.
11. Other programmes and prerequisites¶
Deletion lifecycle, ADR-014 versus amendment J (PH-02, D3-12). ADR-014 (in the uncommitted
.worktrees/bulk-pdf-deletion-lifecycle worktree; product decisions of 12 August) gives project and
search deletion a 24-hour grace period and then physically deletes Project, Search and Study
documents, leaving content-free tombstones; the deletionLifecycle flag on main is that design's
flag. Amendment J (approved 3 October) and QD1 forbid erasing identification history and published
evidence, and the canonical collections are outside ADR-014's deletion scope. Recommended (D3-12):
withdrawing a search hides its Studies but keeps Citations and canonical evidence; deleting a whole
project keeps ADR-014's removal with a tombstone. X-DEL is redefined: ADR-014's manifest and
tombstone model covers the canonical collections for whole-project deletion, and search withdrawal
never physically removes identification history. The register's §2 records the conflict; Q-33 is
re-framed as "which governs for admitted projects".
Search import robustness, X-IMPORT (PH-09, Adopted). P1 adds Citation capture and P2 identifier
matching inside search import, whose parse job still has a 5-minute timeout
(ReferenceFileParseJobConsumer.cs:88, verified); the May incident showed imports above about 3,000
studies fault and duplicate-event saga crashes. #2612 is a docs-only plan ("add staged search
import plan", 1 file, OPEN since 1 September), not an implementation. X-IMPORT (prerequisite of P1):
staged import as #2612 plans, or an equivalent, implemented; a sized timeout with heartbeat and a
stall watchdog; idempotent saga creation; P2's pending-dedup status aligned with staged-pending
status. Acceptance for 5,000- and 50,000-record imports (AC-P1-19). Owner: Study Management
(import) with L12.
Flag overhaul P7 versus R0 admission (PH-14, Adopted). The flag overhaul plans per-project
targeting (P7) and says domain enrolment stays separate from flags. R0's admission record is that
domain enrolment; AF2 production pilots route through P7 targeting keyed to it, so only one
per-project mechanism is built. At F1a the flag programme owner's checklist passes, run by a
fresh-context agent; Chris rules on exceptions (E75).
Authority-transition job classification (PH-15, Adopted). The approved authority transition
classifies every background job family and quarantines jobs it cannot admit, and new consumers need
broker permission rules. New families (publication phase 2, adoption backfills, retroactive dedup,
expiry, lifecycle transitions, outcome recomputation, notification fan-out expansion, batch-opening
evaluation) get an M5/P9 classification and broker rules in C16 and in each release ADR.
AF2 per-project admission slices (DS-04, Adopted; D1-07). Q-25 approved per-project routes for
AF2, the redesigned shell and eligibility, but the plan listed them as other programmes' joins with no
slice. Plan-owned slices reading R0's admission record: AF2 per-project admission (R0 or R2a; the AF2
gate is a pure-function input, annotation-form-v2-eligibility.ts:59-60, per DS); stage-review shell
per-project admission (R2a); eligibility per-project admission (R3a). Every release gets a
"production enablement" step with its own evidence, including turning R1a's editor on by default.
Environment-wide enablement of these flags stays a GA prerequisite. Production opt-in pilots run
before GA under D1-07 (approved 3 October): after staging acceptance, R0's production soak and AF2
per-project admission.
AF2 pdf-tools, X-PDFTOOLS (PH-20, Adopted). AF2 still needs v1's pdf-tools subsystem to
mark, move and delete graph regions and to render cropped regions; that migration is "separate,
unscheduled work", and it also blocks Phase 4 PR 9's cleanup (src/services/web/CLAUDE.md:361,
verified). Canonical extraction (O1) runs on AF2 only and R4c depends on it. New join X-PDFTOOLS → O1
and R4c, owned by the AF2 programme; if the AF2 owner prefers, O1's scope states the gap explicitly
(graph digitisation stays on AF1 for legacy projects) instead.
FEAT-023 Material 3 (D3-01). UI1 applies before FEAT-023's cutover: M3 roles over current components, routes registered for the cutover baselines; the FEAT-023 light cutover sequenced before R2a's reviewer UI if possible; the stack's inbox and preferences screens meet UI1 before testers see them, and its conversation, issue and PDF screens before production.
Bulk PDF and study attention ownership (NS-19). Checked PDFs (#3947) and study issues (#3945) move to the PDF and Study Management programmes; P1 records PDF approval as a retrieval event; bulk PDF finalisation stays in the writer inventory.
12. Revised external joins¶
Replaces integrated plan §5.11. Status at 15:18 BST.
| Join | Needed by | Owner | Evidence | Status |
|---|---|---|---|---|
| X-AF2: AF2 production readiness, or the plan-owned AF2 per-project admission slice reading R0's admission record (Q-25, DS-04) | Any production pilot of R2–R4 reviewer UI | AF2 programme (readiness); L5 with L0 (slice) | Agreed per-flag decision; slice merged with tests | Not met: AF2 on in staging only |
X-SHELL: stageReviewRedesign / stageReviewDockview readiness, or the shell per-project admission slice |
R2a onwards where the new UI extends the shell | Stage-review programme; L5 (slice) | Per-flag decision; slice merged | Not met: off everywhere |
| X-STATS-a: usage family on in staging and pilot projects allowlisted on both hosts | R2c staging pilot under PS1 | FEAT-024 owner | Staging configuration and parity record | Not met: family not built; fold flag now pinned on in staging |
| X-STATS-b1…b7 (§7.2): idle gate (b); soak; production pending index; production rollout approval; production eligibility; usage family built; usage family staging proof | R2c production publication from the materialised family; until then Q-31(b) | FEAT-024 owner; Chris (b4) | Per step | b1 provisional fail; b2 open; b3–b7 not started |
| X-STATS-c: target-aware annotation classification (E71) | R2b pilots on allowlisted projects; R3a (AC-R3a-06) | FEAT-024 owner (+ #3979) | Merged with reconciliation plan executed | Not started; no issue tracks it |
| X-ELIG: S4-B, S4-C, S6a, S6b, fixed-two correction, Project-token redesign, flag delivery to PM (§5.2) | R3a production admission; X-BATCH activation | Eligibility programme (paused); R3a absorbs per D3-09 | Each item merged or extracted; count-only reservation check; AC-ALL-26 (C18-T02) with eligibility on | Not met; programme paused (A-31) |
| X-CLAIMS: production claims route per D3-16; static switch on both hosts; load, failover and orphan backstop; E2E both modes | R2b claim behaviour, R3a reservation admission and any capacity promise, in production | Presence owner with FEAT-024 owner | AC-T-03 to AC-T-07; AC-T-09 (route built); AC-ALL-23 (both tracking modes) | Not met: tracking off everywhere; M15 unowned; #3876 deferred |
| X-RECLAIM: reconciliation-task editor claim working untracked (RA1) | R4a in every environment | L6 with presence owner (internal to the plan) | AC-R4a-36 to 39 | Designed here; frozen at F4 |
| X-AUTH-SCHEMA: membership schema 1 in production (G-D) | R1c | Authorization programme | Production migration evidence | Not met |
| X-AUTH-ENFORCE: M6 staged cutover (G-C), or parity tests | R1c | Authorization programme | Cutover evidence or parity tests | Not met |
| X-AUTH-WP9: explanation endpoints and surfaces (after WP3) | Explanations in R1b; R1c | Authorization programme | Explanation equals enforcement decision | Not met |
| X-AUTH-RESOLVER: out-of-request authority resolver (#3251) | AL1; allocation reviewer validity and Phase 2; R3a per-reviewer preview | Authorization programme with allocation owner | Resolver merged; validity check on save and activation | Not met: #3251 open since 5 September |
| X-BATCH: evidence seam, pool-entry membership, durable opening with pool-entry events, read-only status, performance gate, X-ELIG first (§4.2) | R3c readiness reuse; any batch enablement | Batch programme | Merged slices; benchmark; event fixtures | Not met: #3939 conflicting |
| X-NOTIF: stack steps 1–5 (#3932, #3938, #3941, #3942, #3943) merged with flags off | Notices in R1c (also step 3), R2c, R3c, R4a (also step 6 for conversations), R4b; never a release dependency (feature queues) | Notification programme | Merged code; run:e2e-full on retargeted heads | Not met: nothing merged |
| G-NOTIF (gate): per environment and kind family | Any notification enablement outside e2e and Mailpit | Chris | Approval recorded in the flag audit; N1, N3 delivered | — |
| X-DEL: ADR-014 covers canonical collections for project deletion; withdrawal keeps identification history (D3-12) | P1 search withdrawal | Deletion-lifecycle programme | ADR-014 committed and amended | Not met: ADR-014 uncommitted |
| X-IMPORT: staged import (as #2612 plans) implemented; sized timeout with heartbeat and watchdog; idempotent sagas | P1, P2 | Study Management (import) with L12 | AC-P1-19 | Not met: #2612 is a plan document |
| X-AF2-PR9: AF2 Phase 4 PR 9 | R4c | AF2 programme | Merged code | Not met |
X-PDFTOOLS: v1 pdf-tools migrated to AF2's data-source seam, or O1's scope states the gap |
O1, R4c | AF2 programme | Merged code, or O1 scope note | Not met: unscheduled |
| X-ARCH-a: #3985 non-upsert saves and version bump on direct writes; #3973 awaited events | F1a | Architecture-review programme | Merged with architecture tests | Not met |
| X-ARCH-b: MassTransit-after-v8 decision (#3986) | R0 (PM consumer guards) | Architecture-review programme; Chris | ADR | Not met |
| X-ARCH-c: runtime flag provider fix (#3975) | R0 admission; G-NOTIF evidence | Architecture-review programme | Merged with singleton-identity test | Not met |
| X-ARCH-d: #3979 (fixed-two filter) and #3980 (ValueObject equality, threshold serialisation) | R3a | Architecture-review programme | Merged | Not met |
13. Programme risks¶
| Risk | Mitigation |
|---|---|
| Claims and capacity guards do nothing in production (tracking off everywhere), so R2b/R3a capacity promises cannot be tested there | X-CLAIMS; D3-16 (per-pilot binding-scope tracking recommended); E2E both modes |
| Switching tracking on breaks saves under FEAT-024 writes (typed 503 on mode disagreement) | Binding-scope setting (T3) or the owned M15 transition (T2); static configuration on both hosts |
| Two reconcilers edit one study (no editor exclusion today) | X-RECLAIM in every environment |
| Membership facts go wrong once canonical data leaves Study (re-offered studies, 404 resumes, slot miscounts) | E20 as a per-reviewer projection; E64 seam with truth-table parity; R0 floor reader logic |
| Two reservation migrations touch hub, consumers and fold twice | One migration (E68) with S4-B; count-only check first |
| A flag enabled on one host only (eligibility, allocation) | E75: delivery to both hosts plus an agreement check |
| Batches activate with an O(N) lazy frontier and no durable opening | X-BATCH criteria; performance gate before any enablement |
| Owner decisions recorded only in PR bodies (#3939's denominator) | D3-13b; ledger precedence; PR text treated as PROPOSAL |
| FEAT-024's production chain is longer than the plan assumed | Q-31(b) designed as the first pilot path; X-STATS-b chain with owners |
| Fixed two in the configuration digest makes target-aware counting a migration | X-STATS-c with a reconciliation plan before R2b/R3a |
| The staging fold flag is now on, and project 0102 is both FEAT-024's pilot and a plan pilot seed | D3-10b: keep 0102 out until C8-T07 passes; FEAT-024 STATUS corrected (R11) |
| Notification category skew blocks opt-out during deploys or rollbacks | Tolerant preferences before #3942 merges (N1); C15-T06 |
| Email continues after the flag is turned off; environment-wide flags; runtime overrides by any admin | Delivery halt, per-project admission, inbox reads independent of capture (N3); G-NOTIF; D3-21 |
| Exposure from reconciliation questions leaks into agreement as "independent" | C3 exposure kind; AC-R4a-35, AC-R5c-07 |
| Inbox floods from publications and outdated flags | Flood controls before R2c (N8); recorded fan-out with a threshold |
| Nine stacked PRs with generated-file conflicts and no CI E2E merge badly | Merge protocol (§8.2); run:e2e-full; ADR |
| #3961's roadmap changes foundations F1a freezes and competes for capacity; MassTransit v8 support ends December 2026 | D1-02 joint sequencing; X-ARCH joins |
| Runtime flag overrides are silently reset (#3975) | X-ARCH-c; no evidence relies on runtime overrides |
| Deletion physically removes identification history (ADR-014) | D3-12; X-DEL redefined |
| Imports time out once P1/P2 add work inside the job | X-IMPORT with 5,000- and 50,000-record criteria |
| Presence payloads disclose reviewer identities across blinding | T10; D3-20 |
| Orphaned claims hold capacity indefinitely | Backstop before X-CLAIMS (T9) |
14. Decisions, engineering items and assumptions¶
14.1 Batch D decisions this page depends on¶
| ID | What it decides here | Recommendation used |
|---|---|---|
| D1-01 | #3964 versus #3969 | Decided by Chris (3 October): keep #3964, port #3969's active-member check and tests, close #3969. Carried out: #3964 merged (85e6facf7), #3969 closed |
| D1-02 | Precedence and sequencing with #3961 | Decided by Chris (3 October), as recommended: #3961 Phase 0 continues; X-ARCH-a before F1a; X-ARCH-b before R0; #3988 into E24 or after R2a; #3989 as L5 seam slices |
| D1-03 | ProjectStatistics activate or freeze (#3987); GA counting | Decided by Chris (3 October), as recommended: activate the families this plan uses; not freeze, so Q-31(b) is not extended to GA. The activation date is still to be set (needed for G0) |
| D1-07 | Production opt-in pilots before GA | Decided by Chris (3 October), as recommended: yes, after staging acceptance, R0 soak and per-project admission slices |
| D1-08 | Write-path gate shape | Decided by Chris (3 October), as recommended: ADR-019 gate (b) shape; R10 measures it; the start thresholds apply now and F1a confirms them from M0 evidence |
| D1-09 | Notification merge order and #3965 placement | Decided by Chris (3 October), as recommended: as §8.2; restacking #3965 onto #3944 still needs the stack owner's agreement (A-33) |
| D2-07 | Does a draft hold the place | Middle ground (§6.2) |
| D2-08 | Two tabs | Read-only second tab with "Take over editing" |
| D3-01 | UI1 timing for stack screens | Inbox and preferences before testers; others before production |
| D3-07 | "My work" placement | Project-level in R3c/R4a; global after GA; the badge is flag-independent |
| D3-09 | Eligibility absorption and D8 mapping | R3a absorbs S6b (and others only if still paused); D8 as §5.2 |
| D3-10a–d | Fence read as current; project 0102; multi-profile served live; preview exempt from PS1 | Yes to all |
| D3-11 | Agreement store | Own rebuildable store |
| D3-12 | Deletion versus history | Withdrawal keeps history; project deletion keeps ADR-014 with tombstone |
| D3-13a–f | Allocation and batches | Yes to all (§3.2, §4.2) |
| D3-16 | Production claims route | (a) binding-scope #3876 per admitted pilot |
| D3-17 | Optional capacity cap | Yes |
| D3-18 | Shared-form tracking settings | As §6.2 |
| D3-19 | No claim on a dependent form while screening | Yes |
| D3-20 | Presence disclosure | Counts and own place; names for Monitor holders; never across BL1 |
| D3-21 | Notification enablement | Pilot projects first; G-NOTIF per environment and kind family; halt before production email |
| D3-22 | Emails name the project | Yes, project name only |
| D3-23 | Auto-resolve related notices | Yes |
| D3-24 | Per-project email mute | Yes, before production email |
| D3-25 | Conversations as audit record | Yes |
14.2 Engineering items E64–E75¶
| ID | Contract | Lane / contract | Gate |
|---|---|---|---|
| E64 | Membership-facts seam. Extract IReviewMembershipFacts, behind which the pool predicates, StageWorkloadShareEligibility, AllocationClaimSlot, ActivityReservationAdmission, the own-place checks and the capacity pipelines read per-reviewer facts (own session state per form, claim kinds held, own decision per profile, other reviewers holding a place). Embedded-Study provider first with truth-table parity and no behaviour change; CanonicalSummary provider at R0/R2a (shape in the consistency model) |
Eligibility owner, L1, L7 / C6, C7 | F1a (seam), R0 (floor reads it) |
| E65 | Allocation on canonical stages. Refusal guard (no shares on canonical stages; R0 refuses a stage with an enabled regime) until AL1; AL1 adapter: regime schema v2 (form binding, target source), floor one release ahead, compatibility check, read APIs and editor on the membership projection, D8 slot rule on form-keyed claims, publication of a different target refused while a regime exists | L7, allocation owner / C7 | R0 (guard); F-A, AL1 (adapter) |
| E66 | Requested-review admission and capacity cap. The requestedReview claim written by the AdditionalReviewRequest command (single use, expiring, audited), honoured by pool filters, allocation, typed admission and capacity guards for that reviewer only; target unchanged; counted outside allocation progress; the optional capacity cap (D3-17); the set of sessions that hold a place |
L6, L7 / C7, C9 | F4 (design), R4a (build) |
| E67 | Progressive batches for canonical stages. IStudyObligationEvidence with legacy and canonical providers; pool-entry-based membership with late cohorts; CAS opening and personal grant writing pool-entry events and durable intents; read-only status; performance gate (Next p95 at 100,000 studies and 2,500 batches; AC-R3a-26, AC-R3c-17) |
L7, batch owner, L12 / C7, C12 | F3 (contract), X-BATCH (evidence) |
| E68 | Claim contract v2 and one reservation-key migration. Typed claims (§6.2) unique per (study, kind, scope, reviewer), released when the last page ends; capacity claims on Study, editor claims on their aggregates; keyed by form identity; versioned hub methods, additive DTOs, new commands with old handlers kept ≥ suspension grace + idle timeout; presence index create–read-both–drop; S4-B targets the final key with stage provenance; the consumer inventory in §6.2 | L7 with presence, eligibility and FEAT-024 owners / C7 | F1a (contract), R2b (ship), X-ELIG (migration) |
| E69 | Production claims route (X-CLAIMS). Per D3-16, the binding-scope tracking setting enabled per admitted pilot, or the M15 transition with an admin route rehearsed on staging; API and PM switched together statically; orphan-claim backstop (absolute lease expiry or bounded sweep behind its own flag); load and failover on Bramble; E2E in both tracking modes | Presence owner, FEAT-024 owner, L17 / C7, C16 | Before R2b/R3a production claims |
| E70 | Tracking adapters. R0: tally getter and claim pipeline merge canonical counts and markers; tracking writers and readers in the inventory with tests. R2a: own-place detection via E64; claim release on first explicit Save/Complete in the canonical transaction; presence FormSessionId; dirty = draft-changes flag; draft-aware release (D2-07); draft lease on a stable tab ID that works untracked (D2-08) |
Presence owner with L0, L1, L5 / C5, C7, C16 | R0, R2a |
| E71 | Target-aware annotation classification. Replace the fixed two at StudyStats.cs:353-355, AnnotationThresholds.MinimumNumberSessions (digest input) and StudyRepository.GetSessionFilter (#3979) with the effective target; catalogue bump and digest migration with a reconciliation plan (forced rebuild of allowlisted projects; checkpoints keep identity); ProjectStageConfigurationChange.AffectedFamilies extended |
FEAT-024 owner; architecture-review owner (#3979) / C7 | Before R2b pilots on allowlisted projects; before R3a (X-STATS-c) |
| E72 | FEAT-024 canonical-sources amendment. Technical-plan amendment and ADR: families over canonical collections in the same pinned snapshot (FormVersionUsage, QuestionVersionAnswers at F2; profile families at F5 per D3-10c); scope kinds and key components with tolerant maps; onboarding contract with an N-1 test; #3506; usage over explicit versions with drafts counted authoritatively; the scoped-rebuild-at-pinned-snapshot service API; fence-read identity in the manifest; protocol 5 batched after gate (b) | FEAT-024 owner with L2, L7 / C8 | F2, F5 |
| E73 | C15 v2 in the notification stack. Kind registry via DI; Source sub-document; one capture service (bulk upsert, $setOnInsert); deterministic SourceId and row ID; inline and recorded fan-out (NotificationFanOut plus leased expander); server label, context, availability and state; disclosure hook per channel; unknown kinds leave the ledger row ready one deploy ahead; registry test |
Notification programme; L14 (spec), L8 (hook) / C15, C19 | ADR in W0; frozen at F1b; landed after the base merges, before the first review-workflow kind |
| E74 | Notification enablement controls (G-NOTIF evidence). Tolerant preferences and kind→category map before #3942 merges; declared email→inbox dependency; operator delivery halt; inbox reads independent of capture admission; per-project notification admission (interim registry, then R0 enrolment scope); idle workers; route guards with opt-out reachable; flood controls; retention per E32 | Notification programme with L0 / C15, C16 | Before any enablement outside e2e and Mailpit; flood controls before R2c |
| E75 | Flag delivery and evaluation consistency. reviewEligibilityPolicy and proportionalStudyAllocation delivered to PM (one block replacing #3939's partial one) with a cross-host agreement check before either is enabled; PM-hosted branches inventoried; one flag evaluation per request passed into admission (AP-22) with a test; no gate or evidence relies on runtime overrides until #3975 is fixed; R0 admission is the domain enrolment that P7 per-project targeting keys to |
L0 with eligibility, allocation and flag-overhaul owners / C6, C16 | Before X-ELIG enablement; F1a (P7 alignment) |
14.3 Assumptions A-31–A-34¶
| ID | Assumption | Basis | Cost if wrong |
|---|---|---|---|
| A-31 | The review-eligibility programme stays paused through F3 | Session memory ("paused 25 September until the statistics work finishes"); not recorded in the repository; #3746 had activity on 1 October | If it resumes, X-ELIG slices return to it and R3a's absorbed scope (D3-09) shrinks; if it stays paused, R3a absorbs S6b (and S4-B, S4-C, S6a as needed) and grows |
| A-32 | No legacy untyped slot reservations exist in production, because claims are created only with tracking on, which no deployed environment has enabled | RT-23 code reading; count UNVERIFIED | S4-B must migrate them before eligibility is enabled; an authorised count-only check per environment settles it |
| A-33 | The notification stack owner accepts C15 v2 and lands it after the base merges and before the first review-workflow kind, and restacks #3965 onto #3944 (D1-09) | NS §4; the owner has not been consulted | Each review-workflow kind edits shared notification files; conversations wait for #3947's isolation review |
| A-34 | The architecture-review programme lands #3985 and #3973 before F1a | #3961 Phase 0/1; D1-02 | F1a waits, or canonical repositories carry their own isolated-read, non-upsert and awaited-dispatch discipline enforced by an architecture test, and R0's floor also covers the version-less direct writers (StudyRepository.cs:1306-1319, 1620-1650) |
Resolution record¶
| Finding | Category | Where | Note |
|---|---|---|---|
| AP-01 | Corrected | §3.2; §2.2; E64; contracts C7, open questions E20 note | E20 is a per-form, per-reviewer membership projection carried by CanonicalSummary (consistency model); seam and truth-table parity in AC-M0-04 |
| AP-02 | Adopted | §3.2; §9; §12 X-AUTH-RESOLVER | New join; D3-13d for the interim pool-level Monitor |
| AP-03 | Question | §3.2; E66 | D3-13c; requestedReview claim design ready |
| AP-04 | Corrected | §4.2; E67; contracts C7, AC-R3c-05 | X-BATCH rewritten; evidence seam; pool-entry denominator |
| AP-05 | Corrected | §4.2; E67; contracts C7 | Durable CAS opening with pool-entry events; status read-only; performance gate |
| AP-06 | Question | §3.2; E65; contracts C7, open questions A-09 | D3-13a; refusal guard recommended |
| AP-07 | Adopted | §5.2; §6.2; E68 | One migration with S4-B; consumer inventory |
| AP-08 | Adopted | §5.2; E75; inventory §5 | Flags to both hosts with an agreement check |
| AP-09 | Corrected | §5.2; §12 X-ELIG | X-ELIG enumerated; absorption per D3-09 |
| AP-10 | Adopted | §3.2; E65 | AL1 after allocation Phase 2, before Phase 3; regime schema v2 and floor |
| AP-11 | Adopted | §3.2; §4.2 | Settings reference regime and plan by ID (PROPOSAL at F3) |
| AP-12 | Question | §4.2 | D3-13b; #3939's text stays PROPOSAL until recorded |
| AP-13 | Adopted | §3.2; AC-R6-14 | Adoption rows for target, threshold and enabled regimes |
| AP-14 | Corrected | §7.2; E71; §12 X-STATS-c | FEAT-024-owned prerequisite with reconciliation plan |
| AP-15 | Adopted | §3.2; E65 | Read APIs refuse canonical stages until AL1 |
| AP-16 | Adopted | AC-AL1-04..08 | — |
| AP-17 | Adopted | §3.2; AC-R3a-26 | Selection p95 400 ms PROPOSAL; sampling at F3 |
| AP-18 | Adopted | §4.2; inventory §6 | Two supersession rows |
| AP-19 | Corrected | §4.2; §12 | X-ELIG before X-BATCH activation |
| AP-20 | Adopted | §3.2; change A7 | Route retired in the first L16 PR |
| AP-21 | Corrected | §3.1; inventory §2, §3, §7 | Refreshed; owner asked to update STATUS (A6) |
| AP-22 | Adopted | §3.2; E75 | One evaluation per request, with a test |
| AP-23 | Adopted | §3.2; change A9 | #3269 checklist before F-A |
| RT-01 | Corrected | §6.2; §12 X-RECLAIM; contracts C7, open questions E6, AC-R4a-36 to 39 | X-TRACK replaced; E6 "always" |
| RT-02 | Corrected | §6.2; open questions Q-25 note | Q-25 premise corrected; route is D3-16 |
| RT-03 | Adopted | §6.2; §12 X-CLAIMS; E69 | Joint ownership with FEAT-024 |
| RT-04 | Adopted | §6.2; T11; AC-ALL-23 | E2E in both modes |
| RT-05 | Corrected | §6.2; E70; open questions A-21, AC-R2a-35 to 37 and AC-R2a-06 | R2a adapter |
| RT-06 | Corrected | §3.2; §6.2; contracts C7 | Projection keyed by form with per-reviewer markers |
| RT-07 | Corrected | §6.2; E70; AC-R0-09 | Floor reader logic (mechanics in consistency model) |
| RT-08 | Corrected | §6.2; contracts C16 | Tracking writers and readers in the inventory |
| RT-09 | Question | §6.2 | D2-07 middle ground |
| RT-10 | Adopted | §6.2; T7; E70 | Lease on stable tab ID; take-over per D2-08 |
| RT-11 | Adopted | §6.2; E68; contracts C7 | Claim contract v2 at F1a |
| RT-12 | Question | §6.2; open questions Q-28 | D3-18 |
| RT-13 | Question | §6.2; E66 | D3-17 (cap) with D3-13c (requested review) |
| RT-14 | Question | §6.2; §9 | D3-20; C10 channel listing adopted |
| RT-15 | Adopted | §6.2 | Transaction rows supplied to the consistency model |
| RT-16 | Corrected | §6.2; AC-R2b-03 | Rewritten |
| RT-17 | Adopted | §6.2; open questions Q-20 | Dependency recorded; minimal version drops the warning |
| RT-18 | Adopted | §6.2; AC-R3c-14 | Completion withdraws claims through the outbox |
| RT-19 | Adopted | §6.2; AC-R2c-19 | Target reduction through D6's flow |
| RT-20 | Adopted | §6.2; AC-R1c-10 | Revocation releases claims |
| RT-21 | Adopted | §6.2; open questions E32 | Presence and connection retention |
| RT-22 | Adopted | §6.2 | Slot vocabulary in the copy deck (UX strategy) |
| RT-23 | Adopted | §5.2; A-32; AC-R3a-24 | Count-only check; eligibility before tracking |
| RT-24 | Adopted | §6.2; T9; AC-T-06 | Backstop before X-CLAIMS |
| RT-25 | Adopted | §6.2; contracts C15 | No per-claim notices; scope-keyed revocation events |
| RT-26 | Adopted | §6.2 | Exposure in REST payloads only |
| RT-27 | Adopted | §6.3 T1 | Docs PR before F1a |
| MS-01 | Corrected | §7.2; §12 | X-STATS-b chain; Q-31(b) designed first pilot path |
| MS-02 | Corrected | §7.2; E72; contracts C8 | New families and scope kinds by amendment |
| MS-03 | Corrected | §7.2; contracts C8, AC-R2c-25 | Drafts counted authoritatively |
| MS-04 | Corrected | §7.2; contracts C8, open questions A-08 | Fence read and scoped rebuild API; D3-10a confirms the reading |
| MS-05 | Corrected | §7.2 | Command ledger (consistency model); FEAT-024 reuses CommandId |
| MS-06 | Corrected | §7.2; R1 | Source-write seam (mechanics in consistency model) |
| MS-07 | Corrected | §7.2; E71 | Named FEAT-024 change; three sites |
| MS-08 | Question | §7.2 | D3-10c; R3a default profile projected exactly |
| MS-09 | Corrected | §7.2; contracts C8, C16 | Three mechanisms named |
| MS-10 | Adopted | §7.2; §12 X-STATS-a | D3-10b, D3-10d; staging fold flag noted |
| MS-11 | Corrected | §7.2; contracts C8, AC-P1-07 | PRISMA from authoritative records only |
| MS-12 | Corrected | §7.2; R10 | Per-project sequence removed (brief §1.2; consistency model) |
| MS-13 | Corrected | §3.2; §6.2 | Projection fields and write rules in the consistency model |
| MS-14 | Adopted | §7.2; R3 | Onboarding contract before F2 |
| MS-15 | Adopted | §7.2; R6 | Protocol 5 once, after gate (b) |
| MS-16 | Adopted | §7.2; AC-ALL-04 © | FEAT-024 rollback order in AC-ALL-04 © |
| MS-17 | Adopted | §7.2 | Adoption fences and rebuild; #3845 or manual label |
| MS-18 | Adopted | §7.2; contracts C8 | Canonical designer reads question-version answers |
| MS-19 | Adopted | §5.2; R9 | StageSettings version replaces the Project token |
| MS-20 | Question | §7.2 | D3-11 |
| MS-21 | Adopted | §7.2 | Overview fields extend existing queries |
| MS-22 | Adopted | §7.2; contracts C8 | History never an as-of input |
| MS-23 | Corrected | §7.1; R11; inventory §3, §7 | Refreshed; staging fold flag pinned on (new) |
| MS-24 | Adopted | §7.2; R8; contracts C16 | #3524 after C16; admission record is not the allowlist |
| NS-01 | Corrected | §8.2; E73; contracts C15 | Two capture modes; "no outbox" wording replaced |
| NS-02 | Adopted | §8.2; N1; E74 | Before #3942 merges |
| NS-03 | Adopted | §8.2; N4; E73 | Kind registry and single capture service |
| NS-04 | Adopted | §8.2; N3; E74; §12 G-NOTIF | D3-21 sets the enablement policy |
| NS-05 | Question; answered 3 October | §8.2; N2 | D1-09; #3965's ten pushed commits noted; the legacy cross-stage refusal is in the tenth; the R4a rule is outstanding |
| NS-06 | Adopted | §8.2; AC-R5c-07 | C3 exposure kind |
| NS-07 | Adopted | §8.2; AC-R3c-11, AC-R3c-12, AC-R4a-47 | D3-07 for placement |
| NS-08 | Corrected | §8.2; N9; contracts C16 | Two stack Study writers added |
| NS-09 | Corrected | §8.2; N7; AC-R1c-04 | X-NOTIF for R1c; dashed edges |
| NS-10 | Question | §8.2; N8 | D3-01 |
| NS-11 | Adopted | §8.2; acceptance criteria §7.5 C15-T01 to T11 | — |
| NS-12 | Corrected | §8.2; contracts C15 | Deterministic SourceId; wrong precedent removed |
| NS-13 | Adopted | §8.2; N8 | Before R2c |
| NS-14 | Adopted | §8.2; open questions Q-20 | PROPOSAL narrowing |
| NS-15 | Adopted | §8.2; notifications §3 | Rows and taxonomy |
| NS-16 | Adopted | §8.2; E74 | Live-on-merge list |
| NS-17 | Adopted | §8.2; N12 | Merge protocol; X-NOTIF definition; ADR |
| NS-18 | Corrected | §8.2; N10 | #3944 out of the projection's readers |
| NS-19 | Adopted | §8.2; §11 | Ownership transfers |
| NS-20 | Adopted | §8.2; §9 | Capabilities in C10 |
| NS-21 | Adopted | §8.2; open questions E32 | Stores beyond the inbox |
| NS-22 | Corrected | §8.2; notifications §1.2, §6 | Items marked untracked |
| NS-23 | Adopted | §8.2 | Capture modes per operation (consistency model) |
| NS-24 | Adopted | §8.2 | Issues never carry answer disputes after R4b |
| NS-25 | Follow-up | §8.2 | Stack backlog: remove projectInvitation from email categories |
| NS-26 | Adopted | §8.2; E73 | Unknown kind leaves the row ready, one deploy ahead |
| DS-02 | Corrected | §10; §12 X-ARCH-a–d; plan §8, §10 | Programme added; sequencing via D1-02, D1-03; D1-01 decided |
| DS-04 | Adopted | §11; §12 X-AF2, X-SHELL | Plan-owned per-project admission slices; D1-07 |
| PH-02 | Question | §11; §12 X-DEL | D3-12 |
| PH-04 | Question | §5.2 | D3-09 (D8 mapping); truth table extended |
| PH-09 | Adopted | §11; §12 X-IMPORT | #2612 verified as a plan document only |
| PH-14 | Adopted | §11; E75 | R0 admission is P7's domain enrolment |
| PH-15 | Adopted | §11; contracts C16 | M5/P9 classification and broker rules |
| PH-20 | Adopted | §11; §12 X-PDFTOOLS | Or O1 states the gap |
| DC-05 | Corrected | §5.2; R9; §12 X-ELIG | No per-project document per admission; StageSettings-version default |
| DC-21 | Corrected | §7.2; contracts C8, C16 | Admission refuses transactional point mode |