Notification infrastructure integration¶
Temporary planning document; planning only. Chris added the existing, current and planned notification infrastructure to this programme's scope on 3 October 2026. This is not authorisation to implement notifications, to send messages to project users or to change the notification PRs. The notification stack belongs to a separate programme: a local conversation ("SyRF Notifications and Study Issues") authorised its implementation on 2 October and delegated it to the Juniper thread "SyRF: Inspect notification and email infrastructure". This plan consumes its contracts and proposes amendments; it does not take it over.
Status (24 October 2026): the notification implementation restarted, with Chris's yes. The hold of
5 October no longer applies to the notification stack (#3932 to #3947 and #3965). The restart follows
the implementation plan at /home/chris/workspace/syrf/handover/notifications-implementation-plan-2026-10-24.md (outside the repository, so not linked). Its first step, N0, is
this set of spec amendments. Chris decided seven inbox questions the same day (§8). D45 to D50 were decided on 26 October 2026 (§8.1); no decision remains pending (§8.2).
Restarting the code does not enable anything. Every flag stays off, no email is sent and nothing
reaches production. Text below that says "planning only" or "on hold" describes the position before
24 October and is kept for history.
Evidence: a read-only sub-agent inspected the eight PR worktrees (each confirmed clean and at its
PR head) and the provenance transcript (targeted excerpts only). PR states were re-read live with
gh on 3 October at about 04:52 BST. File references below are in the top-of-stack worktree
pr/pr3947.checked-pdf-proposals-and-explicit-administrator-a-r6bdl2 unless stated. The
adversarial reviews re-checked the #3944 behaviours in its own worktree.
Owner session of 4–5 October 2026. Chris decided the communication questions in package O2 on 4 October: D3-22 (email content) is Decided-amended, and D3-24 (per-project email mute) and D3-25 (conversation retention and export) are Decided. D3-21 (enablement gates) and D3-23 (notice resolution) are Brief items, not owner questionnaires. The detailed rules are in the access, communications and deletion specification (ACD spec §3.5 to §3.9, ACD-R24 to ACD-R32), summarised in §7 below. The informative-email amendment is OS-A26 in the owner-session integration. These are planning approvals only. Notification delivery remains unauthorised: nothing here enables a flag, admits a project, sends an email or changes a notification PR. Brief approval and implementation authorisation are separate (D1-04) and on hold since 5 October 2026. The consolidation §6 wins over older text on this page; earlier wording is kept and marked where it is superseded.
1. Verified baseline¶
1.1 What is merged on main (78c6d097d, unchanged at 2949ca3a7)¶
- Email transport: SES by default, SMTP to Mailpit outside production,
SendSimpleEmail, development routing (api/Models/ApiEmailTransportRegistration.cs:23-44,api/Models/AwsEmailService.cs:190-192, 293-307). - The SignalR hub with per-user groups, plus live, unsaved banners: surplus warning,
ActivityClaimRevoked, disconnection/idle (web/core/services/signal-r/signal-r.service.ts:147-154). - A dead legacy
Notificationclass (InvestigatorAggregate/Notification.cs), unused. - The Identity service's recovery-email outbox, an earlier pattern for leased, no-retry sends.
1.2 What exists only in the open stack (all open, stacked, flags off, none merged)¶
States as read on 3 October at 15:18 BST, except #3965, re-read at 20:35 BST. Re-checked on
5 October 2026 at 00:33 UTC (gh pr view for every PR below): every state and head is unchanged,
3932 is still conflicting with main and nothing in the stack is merged.¶
| PR | Head | Adds | State |
|---|---|---|---|
#3932 (base main) |
2ddd96fb0 |
InboxNotification aggregate (pmInboxNotification), inbox API and /inbox UI, export-completed capture in the terminal export transaction, change-stream InboxChanged hint |
Conflicting with main (generated files only) |
| #3938 | 180f0d1bb |
Import-completed (recipient fixed at upload admission) and project-invitation capture; typed ISourceInboxCapture |
Mergeable in stack. 24 October 2026, restart step L2: merged onto the amended L1 (#3932); reads notificationInbox only through IInboxAdmission; the consumer contract names Project Management, with an API/PM parity test; merged into main as 864fc8bbc (ADR-026 decisions 7 and 8) |
| #3941 | eb72c886d |
reviewAccessGranted and workloadChanged from before/after snapshots of effective Review grants and workload |
Mergeable in stack; wraps ProjectController.UpdateProject, which #3964 (merged 3 October) also edits. 25 October 2026, restart step L3: merged onto the amended L2 (#3938) with #3964's owner-only rule kept ahead of capture; reads notificationInbox only through IInboxAdmission, once per operation; D1-09's refused-grant test added. 27 October 2026: base is main now that L2 has merged; keyset paging and lighter polling (#4547) added; in review (ADR-026 decisions 9 and 10) |
| #3942 | 75f89b33d |
Opt-in email preferences per category (default off), immediate delivery with a per-item ledger, leases and ambiguous-send handling | Mergeable in stack |
| #3943 | d75fd81b9 |
Durable daily digests (IANA timezone, DST handling, at most 100 unread items frozen when claimed) | Mergeable in stack |
| #3944 | 59ab32e7b |
StudyConversation: reconciler questions to selected candidates and replies (studyAttention flag) |
Mergeable in stack |
| #3945 (on #3944's branch) | 25b364da3 |
StudyIssue reports with open/resolved/reopened status and administrator resolution (studyAttention); writes Study bibliographic fields |
Mergeable in stack |
| #3947 (on #3945's branch) | ae3c6749d |
Checked PDF proposals (quarantined, scanned, administrator-approved), using the study-issue kind; writes the Study PDF path; adds native tools to the API image | Mergeable in stack |
| #3965 (on #3947's branch) | 986b1cdc2 |
Chris's Q-10 changes: reconciliationConversations flag depending on notificationInbox; one-to-one threads; completed non-reconciliation sessions only; a content-free "questioned" marker; read-only context; all-or-nothing thread creation; stable reviewer labels; typed reply refusals; a reconciler who reviewed the study in any stage is refused |
Open and ready for review; ten commits pushed (20:35 BST); its local e2e journey passed (19:30 BST reading); waits for the stack (D1-09) |
Checks and review: E2E jobs were skipped on every stack PR (no run:e2e-* label), so the stack's
new E2E specs have never run in CI. Product CI runs only while a slice is temporarily retargeted to
main. The latest-head Claude reviews were short reviews of main-merge deltas; #3932 has no human
approval. Issue #3950 lists 13 follow-ups; it does not track retention, fan-out limits, unsubscribe,
the email flag dependency, a delivery halt or per-project admission (those are untracked).
Live on merge, 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 (the change-stream watcher, the email worker and the digest worker) in every API replica; native PDF tools in the API image (#3947).
1.3 Contract facts the review programme must respect¶
- The saved inbox row is the durable obligation. It is written in the same Mongo transaction as
the source change (snapshot read, majority write);
SourceInboxCapture.cs:58-60refuses writes outside a source transaction. Under C15 v2 that remains the inline mode; events with no such transaction use recorded fan-out (a durable intent expanded by a leased worker). There is no second notification store, and in-memory outboxes stay forbidden. - Deduplication uses the unique index (RecipientId, Kind, SourceId) with an insert-only upsert
(
SourceInboxCapture.cs:62-71,InboxNotificationRepository.cs:38-39), plus deterministic SHA-256 identifiers for explicit-request workflows (StudyConversation.cs:20-25,StudyIssue.cs:24-27). A second event with the same SourceId is silently dropped, so SourceId must identify the event occurrence deterministically. Done 27 October 2026 (#4530, M17):reviewAccessGrantedno longer usesGuid.NewGuid(); itsSourceIdis SHA-256 over the kind, project, stage and the base project version, so replaying a grant (or a resumed batch) adds nothing, and a later grant adds a new row. - A Kind is also an email category key, and categories are validated as an exact set. Adding a
kind needs the constant on
InboxNotification, aNotificationDetailResolverbranch with an access class, an entry inNotificationEmailPreferences.CategoryNames, the Angular preferences list and an inbox action label. Because the set is exact, every new kind breaks preference saves (including opt-out) whenever browser and API disagree; without the resolver branch the inbox shows "Related item unavailable" and the email is silently suppressed. Amended 24 October 2026: a kind may declare the email categorynone(in-app only); such a kind is outside the preference category set (C15 v2). - Payloads are generic: titles and links only. Protected detail is shaped at read or send time by the resolver, with fresh authority checks and no application-admin shortcut for review items.
- Recipients are individuals expanded at write time.
Revocation hides details but keeps the item in history and in the unread count.Superseded 24 October 2026 (owner decision 1; supersedesPROPOSALB8): revocation hides details. The item stays in history as "no longer available". It leaves the unread count and the badge. The same applies when conversations are switched off and when the project is deleted. People granted access after an event get no item for it. - Read state never resolves a workflow. Workflow state belongs in the feature aggregate.
- Email: opt-in per category, immediate or daily, one ledger row per item, ambiguous sends never retried, no unsubscribe link yet. The workers run only in the API; the Project Management service's email service throws, and that service doesn't receive the notification flags.
- Flags:
notificationInbox,notificationEmailandstudyAttention(which requiresnotificationInbox) all default off, are environment-wide and reach only the API and web.studyAttentionadmits conversations, study reports and authorised decisions together; #3965 moves conversations to their ownreconciliationConversationsflag.notificationEmaildoesn't declare its dependency onnotificationInbox. The workers run regardless of flags, and accepted immediate and digest mail keeps sending afternotificationEmailis turned off. In staging and preview any SyRF administrator can override these flags at runtime. - No retention, TTL, archive or delete exists for any notification collection.
- #3944 reads legacy sessions. It finds candidates and authorises replies through
Study.ExtractionInfo.Sessionsand stores legacy session IDs (StudyConversationAccess.cs:46, 52;StudyConversation.cs:28), so it would be inert for canonical sessions, and its references would dangle after adoption. - Five inbox writers use different identity rules (
SourceInboxCapture,DataExportJobRepository,StudyConversationRepository,StudyIssueRepository,CheckedPdfProposalRepository); the study-issue and PDF writers useInsertOnerather than an upsert. - Inbox reads are gated by the capture flag: turning
notificationInboxoff returns 404 for every inbox read, which breaks links already emailed. - Access is checked against the captured
StageId, which fails for a study × form task reachable through several stages (RE4). - Extension points are per-kind code in shared files (resolver switch, typed fields on the row and DTO, a browser label chain whose fallback labels any new kind "Open conversation"), so every lane adding a kind would edit the same notification-owned files. C15 v2's kind registry replaces this.
2. #3944 reconciliation questions versus confirmed decisions¶
3944 is real runtime code (aggregate, repository, fresh authorization, API, Angular UI,¶
transactional notifications, tests), not a proposal. It implements a reconciler → selected candidate question-and-reply channel. Measured against the owner ledger:
| Behaviour in #3944 (as inspected) | Confirmed decision affected | Integration finding |
|---|---|---|
Every participant sees every post, and replies notify all other selected reviewers (StudyConversationRepository.cs:64-70; study-conversation.component.html:12-25) |
VS1 candidate isolation ("other candidates' individual answers remain hidden") | A reviewer's explanation of their answer reaches the other candidates. Conflict. Proposed: reviewer-private (one-to-one) threads. |
Incomplete sessions are eligible candidates (StudyConversationAccess.cs:52), and the reviewer's context link opens the editable review route (StudyConversationsController.cs, ContextPath to /review/) |
PV1/VS2 principle: record what a contribution was exposed to | A candidate can be questioned before completing, or after a reopen, and then change their answers, with nothing recorded. VS2 itself concerns accepted answers, so this is an independence and exposure risk extending that principle. Proposed: an exposure marker on any questioned session plus a read-only context link. |
Any Reconcile holder can start a thread; only their own session is excluded from the candidates (StudyConversationAccess.cs:23-35, 49-56) |
"A reconciler is never offered a study they reviewed" (R4a acceptance; the ledger allows audited self-review only for query review, QY4; project-level exceptions are Q-36) | Gap. Proposed: a reconciler-eligibility check before a thread can start. |
Thread keyed by stage; owned by the creating reconciler (StudyConversation.cs:8-29; access :37) |
RE4 (one task per study × form across stages); RA4 (release and reacquire) | A shared form reconciled through another stage, or a reconciler taking over after release, can't see the discussion. Gap. Proposed: bind threads to the reconciliation task identity once R4a exists; allow authorised handover. |
| Generic labels ("Reviewer N") whose numbering differs between picker and thread (#3950 item 10) | BL1 (stage-owned, consistent identity blinding). Owner session, 5 October 2026: superseded; the annotation form owns reconciliation blinding and the screening profile owns it for screening reconciliation, blinded by default (OS-A02) | Gap. |
| No reconciliation-state check; usable after gold exists | QY1–QY9 (audited, per-concern query lifecycle) | Risk of an informal query channel that bypasses the audited lifecycle. Gap. Proposed: threads only while a reconciliation task is open; post-gold challenges go to queries (R4b). |
| Additional reviewers become candidates once they start a session | RA5 (independent additional review) | An extra reviewer could be questioned before completing. Conflict. Proposed: exclude requested additional reviewers until they return their assessment. |
Decision (Q-10, Chris, 3 October): make these changes. #3965 implements them in ten commits
(head 986b1cdc2, pushed and ready for review at 20:35 BST on 3 October; its local e2e journey passed
at the 19:30 BST reading): conversations move to their own reconciliationConversations flag, which declares
its dependency on notificationInbox, and studyAttention's description no longer mentions
conversations; threads are one-to-one between the reconciler and one candidate and are created
all-or-nothing; only completed non-reconciliation sessions are candidates, which also keeps requested
additional reviewers (RA5) out until they return their review (answering NS's Q-N9; recorded in the
register, no question); a content-free "questioned" marker is shown for every reconciler; reviewer
labels are stable; the reviewer's context link is read-only; refused replies are typed; a reconciler
who reviewed the study in any stage is refused. Still outstanding:
- Cross-stage reconciler refusal (legacy part done). The tenth commit (
986b1cdc2) refuses a reconciler with any non-reconciliation annotation session on the study, in any stage and any status. That is stricter than the legacy rule proposed here (refuse a reconciler with any non-reconciliation session on the study in any stage whose questions overlap). Still to come, from R4a: refuse any session on that study × form (AC-R4a-21). - Canonical scopes. Conversations read legacy sessions, so they refuse canonical scopes through
R0's ownership record until R4a rebinds them to
ReconciliationTask; L6 ownsStudyConversationfrom R4a. - Exposure. The "questioned in reconciliation" record feeds C3's exposure states, R5c and R6 manifests (AC-R4a-35, AC-R5c-07).
Merge route (D1-09, approved by Chris on 3 October): restack #3965 onto #3944 and merge it straight after #3944, before #3945 and #3947; if the stack owner prefers to keep it on #3947, it lands in the same merge train before any environment enables conversations. Task identity, handover, stable aliases and open-task-only threads remain R4a follow-ups. Owner session, 5 October 2026: "stable aliases" now means labels that stay fixed within one reconciliation context and are drawn afresh for every Study, under the form's or profile's blinding (RS-R25 to RS-R27); no alias carries across Studies. Conversations are retained as permissioned project audit records (D3-25, §7).
3. Review-workflow notification catalogue¶
None of these triggers exists yet. Each is delivered by the lane and release shown through C15 v2 (contracts). Feature-owned queues (§4.1) are the source of truth; these notices only add delivery. Every kind registers its email category, capture mode, occurrence key and scope.
Email categories (PROPOSAL; the eight existing kinds keep their own keys):
Content policy (owner session, D3-22 decided-amended, 4 October 2026). Every category below is
shaped at send time by the kind's resolver, the disclosure policy and the project's
NotificationContentPolicy (ACD spec §3.5). Emails and digests may carry the project name and study
titles. Aliases and every other item follow the recipient's current permissions and the relevant
form's or profile's blinding. Answer values and free text are configurable per project and off by
default (PROPOSAL, ACD-R27). A recipient who has lost access gets no email. Sent email cannot be
recalled. A recipient who muted the project gets no email from it, and the inbox row is still
captured (D3-24).
| Email category | Kinds |
|---|---|
| Access and workload (existing two keys kept) | Review access; Reconcile access (activity field); workload, including canonical workload changes from the form target and the AL1 plan |
| Work assigned to me | Assignment created, expiry warning, expired, released; additional review requested, withdrawn, expired; concern needs applicability review. Owner session: assignment expiry is optional and off by default (Q-30); an additional-review request raises the effective Study and form target and may name people or groups (consolidation §1); adjudication assigned to me or my group (OS-A13); a merge conflict delegated to me (DM spec); training attempt assessed or retry decided (TI spec) |
| Results of my requests | Additional review returned; concern outcome (QY6); addressed by update (QY8); change approved or declined (LC1) |
| Approvals I need to give | LC1 change awaiting approval; outcome-migration manifest awaiting approval. Owner session: training attempts awaiting manual assessment (TI spec); external screening runs awaiting acceptance (RI spec) |
| Changes affecting my reviews | Form version published (one kind); FV4 revision; profile version published; outdated annotations caused by someone else; task held; inputs changed. Owner session: the "form version published" kind also covers target-only publications (OS-A12) and the attributable generated session versions a publication may now write (D2-01 decided-amended); contribution exclusion notices to the authors of dependent results (ACD-R10); Study left the stage pool while I had started work (SP spec §4.1); duplicate merged or unmerged where I hold evidence or a draft (DM-AE18); accepted external screening decisions that change outcomes I depend on (RI spec) |
| Project changes | Adoption cutover; pilot admitted, removed or made read-only; publication applied or stalled (to the publishing admin). Owner session: "adoption cutover" now covers universal baseline conversion waves and their notice period (BC spec §7); project deleted and project restored (ACD-R15) |
| Existing kinds | Exports; imports; invitations (no notification email: the invitation email already goes, NS-25); conversations (one-to-one; legacy until R4a); study issues |
| Feature release notices (24 October 2026) | featureRelease (D42 C, 6G.N): in-app only. It has no preference key and no email category (kind category none) |
| Event | Recipients | Capture mode | Occurrence key | Scope | Default channel (PROPOSAL) |
Release |
|---|---|---|---|---|---|---|
| Form version published (re-answer required, autoUpdate applied or no action), one kind with the per-recipient effect worked out at read time | Owners of affected current sessions, once per recipient per publication | Recorded fan-out from the frozen manifest | Publish operation ID | Canonical | In-form Needs updating; in-app notice; email per preference | R2c |
| Publication applied, completed or stalled | The publishing admin | Inline | Publish operation ID and phase | Canonical | In-app | R2c |
| Older notices for the same form superseded by a newer publication | Earlier recipients | Inline (state change on existing rows) | Form ID and publication sequence | Canonical | Marked superseded | R2c |
| Session contains outdated annotations caused by someone else (support on-behalf-of write, adoption remap or shared-gold revision; Q-20 narrowed; |
Session owner, one per cause | Inline for one session; recorded fan-out for remaps | Cause ID (command, remap or gold snapshot) | Canonical | In-app flag on the session; optional notice | R2d |
| Requirement revised so re-answering is no longer needed (FV4) | Owners of sessions asked to re-answer | Recorded fan-out | Policy revision | Canonical | In-app | R2d |
| New screening-profile version published with action needed (Q-26) | Reviewers with decisions under prior versions, if the treatment needs action | Recorded fan-out | Profile publish operation ID | Canonical | In-app | R3b |
| Completed-stage change awaiting approval (LC1) | Admins holding the approval capability for every affected stage | Inline up to the cap, otherwise recorded fan-out | (Change request ID, opened) | Canonical | "Changes awaiting approval" queue; badge; in-app; resolved for all once one admin decides (D3-23, now a brief item: resolution reason and resolver reference recorded, history kept; ACD spec §3.7) | R3c |
| Change approved or declined (LC1) | Requester | Inline | (Change request ID, decision) | Canonical | In-app | R3c |
| Drafts or corrections blocking automatic completion (LC1, reviewer side) | Owners of the blocking drafts or corrections | Recorded fan-out | (Stage ID, readiness evaluation) | Canonical | In-app | R3c |
| Stage completed or reopened (owner session: an automatic Completed stage is recalculated as soon as remaining work appears; a manually completed stage stays Completed until an explicit reopening, Q-02) | Stage admins | Inline | (Stage ID, status-history entry) | Canonical | In-app | R3c |
| Reconciliation assignment created, expiry warning, expired, released (RA2–RA4), one occurrence each (owner session: expiry of unstarted assignments is optional and off by default, Q-30; no warning or expiry notice exists unless an admin enables it) | Assignee; on release also the previous holder | Inline; warnings and expiry through a scheduler marker on the assignment | (Assignment ID, transition, version) | Canonical | "Assigned reconciliation work" queue; in-app; email per preference | R4a |
| Additional review requested, withdrawn or expired (RA5) (owner session: the request raises the effective Study and form target, may name people or groups and many Studies, and counts qualifying reviewers once across routes; consolidation §1, RD spec) | The requested independent reviewer; owner session: each named person or eligible group member, one notice per assignee per request listing their Studies; the requester gets progress and completion | Inline; expiry through a scheduler marker | (Request ID, transition) | Canonical | "Requested reviews" queue; in-app | R4a |
| Additional review returned | The requesting reconciler | Inline | (Request ID, returned) | Canonical | In-app | R4a |
| Reconciliation clarification question or reply (#3944, #3965) | The thread's two participants | Inline | Thread and post IDs (deterministic) | Legacy until R4a, then canonical through the task | Existing kinds | R4a alignment |
| Item held because a publication made it incompatible | Reconcilers with access to the task | Inline | (Task ID, held episode) | Canonical | Pool banner (SignalR); in-app | R4a |
| Corrected input needs a re-check ("inputs changed · re-check") | The reconciler or task holder | Inline | (Task ID, drift episode) | Canonical | Task state; in-app | R4a/R4b |
| Query raised on an accepted answer | Holders of the query-review capability for the stages reaching the task | Inline up to the cap, otherwise recorded fan-out with a capability selector | (Query ID, concern ID) | Canonical | Query queue; in-app | R4b |
| Concern resolved, rejected or "addressed by update" (QY6, QY8) | Each raiser individually | Inline | (Concern ID, outcome version) | Canonical | "My concerns and outcomes"; in-app; email per preference | R4b |
| Concern needs current-applicability review (QY9) | Assigned query reviewer | Inline | (Concern ID, applicability check) | Canonical | Query queue; in-app | R4b |
| Review or Reconcile grant changes | Recipient | Inline, capped (bulk group edits use recorded fan-out) | (Mutation command, stage, activity) | Both | #3941's access snapshot, extended with Reconcile grants and the active decision path | R1c, R4a |
| Adoption cutover; pilot admitted, removed or made read-only (U28) (owner session: R6 becomes universal baseline conversion waves with a notice period; quarantine and routing rollback send admin notices; a deleted project converts without member notices, BC-R38) | Project members | Recorded fan-out | (Wave or admission version) | Both | In-app | R0/R6 |
| Outcome-migration manifest awaiting approval (O2) | Owner or migration delegate | Inline | (Manifest ID, version) | Canonical | In-app | O2 |
Rows added by the owner session (5 October 2026). Each row is capture only. Delivery stays
unauthorised until G-NOTIF (§5, D3-21). Occurrence keys, scopes and channels are PROPOSALs for the
owning brief.
| Event | Recipients | Capture mode | Occurrence key | Scope | Default channel (PROPOSAL) |
Release |
|---|---|---|---|---|---|---|
| Contribution exclusion committed (OS-A24) | Authors of dependent results (accepted results, adjudications, hint adoptions) whose inputs changed; the excluded member only if the admin opts in (off by default, PROPOSAL) |
Recorded fan-out from the exclusion operation | (Exclusion ID, recipient) | Canonical | In-app; My work row where action is needed | With each exclusion scope (ACD spec §10: form R2a, stage R3a, profile R3b) |
| Project deleted; project restored (OS-A25) | Active reviewers, reconcilers and admins of the project | Recorded fan-out | (Deletion or restoration record ID) | Both | In-app; existing notices for the project leave unread counts and stay in history as "no longer available" while it is deleted (owner decision, 24 October 2026; PROPOSAL, AC ambiguity B8 |
X-DEL (amended deletion-lifecycle programme) |
| Study left the stage pool while the reviewer had started work (OS-A05, OS-A06) | Reviewers with started work on the departing Study | Recorded fan-out from the pool transition or sweep | (Departure event ID, reviewer) | Canonical | In-app; Saved work row | R3a |
| Duplicate merge conflict delegated to the original reviewer (OS-A29) | The delegated reviewer | Inline | (Merge conflict task ID, delegation) | Canonical | My work row; in-app | P2b |
| Duplicate merged or unmerged (OS-A29) | Reviewers and reconcilers with evidence or drafts on an input Study | Recorded fan-out from the merge or unmerge manifest | (Merge or unmerge ID, recipient) | Canonical | In-app (DM-AE18) | P2a, P2b |
| Adjudication assigned or reassigned (OS-A13) | The assigned member, or each eligible member of the assigned group | Inline | (Adjudication task ID, assignment version) | Canonical | My work row; in-app | R4p |
| Training attempt assessed; retry decided; admission recorded (OS-A18) | The trainee | Inline | (Attempt ID, assessment or admission ID) | Canonical | In-app | Lane TR1 (rollout plan) |
| External screening run accepted and outcomes changed (OS-A20) | Active reviewers whose work depends on the changed outcomes | Recorded fan-out from the acceptance operation | (Run ID, acceptance phase, recipient) | Canonical | In-app | XS1 (proposed lane) |
Row added on 24 October 2026 (inbox restart; D42 C). Capture only, in-app only.
| Event | Recipients | Capture mode | Occurrence key | Scope | Default channel (PROPOSAL) |
Release |
|---|---|---|---|---|---|---|
| Feature release confirmed (D42 C) | People the feature is available to (permission, then targeting) | Recorded fan-out; a release notice is created by the MaterialiseWhatsNew outbox handler (D47 A, decided 26 October 2026) |
Announcement ID and revision | Global or project | In-app only | 6G.N |
Live banners (publication in progress, claim revoked, idle) use the existing per-user SignalR groups, not C15. Routine claims never notify (RT-25).
4. Integration contract (C15 v2)¶
The contract text is C15 in contracts. For lanes adding a review-workflow kind:
- Register the kind (category, scope, admission flag, inline limit, resolver) and its disclosure
fixtures; the registry test fails otherwise. Never edit the shared resolver switch or add a typed
field to
InboxNotification. - Choose the capture mode per operation: inline inside the domain transaction with bounded recipients (Save/Complete: task holder, requesting reconciler; gold publication and query resolution: raisers), or recorded fan-out keyed by durable identity (publications, adoption, bulk grant changes). Benchmark hot paths with capture on.
- Derive the occurrence key from durable identity; never a random SourceId.
- Expand recipients with current authority when the row is written or the intent is expanded; exclude the actor; per-raiser notices go only to the raiser. Feature queues handle later joiners.
- Shape details through the kind's resolver and the disclosure hook, for HTTP, inbox and both
email processors; on any authority miss show "Related item unavailable". Owner session
(D3-22, 4 October 2026): after the disclosure hook, both email processors apply the project's
NotificationContentPolicyat send time (ACD spec §3.5, §4.9). The policy can only narrow what disclosure allows. A recipient who lost access between capture and send gets no email. - Live updates: nothing to add; the invalidation worker covers any inbox write (with jittered refetch, §5).
- Email: map the kind to one of the categories in §3; no new ledger, worker, digest, Quartz job, preferences store or transport.
- Flags: new writes behind a default-off flag that declares its dependency on
notificationInbox; capture also requires per-project notification admission. Reading accepted history stays available, still authority-checked. Producers in the Project Management service use C15 v2's capture service (not a host-specific path). - Workflow state stays in the feature aggregate, never in
ReadAtUtc. - Tests: C15-T01 to C15-T11 (acceptance criteria §7.5; the NS review's AC-C15-01 to 09 are C15-T01 to T09, and AC-C15-10 is C15-T11 with AC-ALL-04 (d)): capture rollback, replay and resume idempotency, registry, disclosure per channel and role, admission, version skew, halt, fan-out, coalesced hints, rollback.
- Never send test mail to users. Use Mailpit and the hermetic e2e stack.
- Restart amendments (24 October 2026). A kind may declare the email category
none(in-app only). A kind with global scope is not subject to per-project admission; its recipients come from feature availability. The view carries a headline, a key fact, typed body blocks and inline actions; an unavailable view shows the kind only (C15 v2 "Source and view"). Dismiss is personal and never resolves work (C15 v2 "Per-recipient state").
4.1 Feature-owned queues (source of truth and fallback)¶
Confirmed obligations never depend on the notification stack being merged or enabled:
| Queue | Carries | Release |
|---|---|---|
| My concerns and outcomes | QY6 per-raiser outcomes; QY8 "addressed by update" | R4b |
| Changes awaiting approval | LC1 admin alert and confirmation | R3c |
| Assigned reconciliation work | RA2–RA4 assignments, expiry and release | R4a |
| Requested reviews | RA5 additional reviews | R4a |
| Owner session: Saved work outside the pool | Started work a reviewer may still finish after the Study left the stage pool (OS-A05; SP spec §3.9) | R3a |
| Owner session: Adjudication assigned to you or your group | Screening adjudication tasks (OS-A13; RS spec §3.9) | R4p |
| Owner session: Merge conflicts delegated to you | MergeConflictTasks (DM spec) |
P2b |
| Owner session: Training to complete, retry or assess | Training attempts and manual assessments (OS-A18; TI spec) | Lane TR1 |
| Owner session: External runs awaiting acceptance; conversion findings | Admin queues from the RI and BC specs | XS1; R6 |
The full set of My work rows by role is in the UX specification §3.5.
Surfacing without notifications (NS-07, PROPOSAL; placement per D3-07): owner session:
D3-07 is Decided (U3, 4 October 2026): project My work, a cross-project tab and a global badge,
even when notifications are muted, with pending admin changes shown; the full global landing page is
deferred. The original proposal follows: a global navigation
badge whose counts are computed at read time over the four queues; a cross-project "My work" tab beside
"Pending Projects" on the project index; a project-overview banner for admins with pending LC1 requests.
With every notification flag off, a user reaches an assignment in a project they have not opened from
the project index in one step, and an admin with a pending request sees the badge without opening the
project (AC-R3c-11, AC-R4a-47). Each release's acceptance tests pass with every notification flag off.
5. Dependencies and gates¶
| Need | Depends on | Gate |
|---|---|---|
| Notices for review-workflow events | X-NOTIF: stack steps 1–5 (#3932, #3938, #3941, #3942, #3943) merged with flags off, and C15 v2 landed before the first review-workflow kind | Per release. Without it, features still ship because the queues in §4.1 carry the confirmed obligations |
| Any enablement outside the e2e stack and Mailpit | G-NOTIF: Chris approves each environment and kind family (D3-21, now a brief item: pilot projects first, broader admission only after a pilot exit review; ACD-R30, ACD-R31); the delivery halt, inbox reads independent of capture and per-project notification admission delivered (E74); no reliance on runtime overrides while #3975 is open (re-checked open on 5 October 2026) | Per environment and kind family |
| Preference saves across versions | Tolerant preferences and the kind→category map, before #3942 merges (or one deploy before the first new category) | Before #3942 merges |
| Enabling conversations | #3965 (ten commits pushed and ready for review; includes the legacy cross-stage reconciler refusal) reviewed and merged after #3944 (D1-09) | Before enabling; F4 |
| R1c group and grant changes | #3941 merged (step 3), aligned to the active decision path with Reconcile grants, with bulk fan-out through recorded fan-out | R1c |
| Canonical sessions in #3944 | Conversations refuse canonical scopes until R4a rebinds them to ReconciliationTask; their aggregates in R6 manifests |
R2a (refusal), R4a, R6 |
| High-volume events (R2c publication, R2d outdated flags) | Recorded fan-out with a recipient threshold; flood controls (mark-all-read, filters, context line, hide unavailable, resolved and superseded state, grouped digests, jittered refetch) | Before R2c |
| Email for new kinds | notificationEmail dependency declared; unsubscribe policy; per-project email mute (D3-24, decided 4 October 2026) |
Before any email category is enabled outside Mailpit |
| Owner session: email content | The project NotificationContentPolicy with send-time access and blinding checks (D3-22 decided-amended; ACD spec §3.5) and per-project mute (D3-24) are prerequisites for every email category, existing or new |
Before any email category is enabled outside Mailpit |
| Owner session: notice resolution | Resolution intent and dispatcher; reasons decided, completed, withdrawn, expired, superseded; unread count means unread and unresolved; history kept (D3-23 brief item; ACD spec §3.7, §4.10) | With the first kind whose workflow item resolves (R3c approvals) |
| Stack screens | UI-1 to UI-11: inbox and preferences before testers see them; conversation, issue and PDF screens before production (D3-01, now a brief item under T-UX-00). Decisions 3 to 6 (24 October 2026) replace the inbox detail page with the expanding card, bell panel and calm header |
Per D3-01 |
Landing sequence from 24 October 2026 (replaces the merge order below). Source: the implementation plan
at /home/chris/workspace/syrf/handover/notifications-implementation-plan-2026-10-24.md, §3. Rules: no rebases; merge main into each branch, oldest first; retarget a child to
main when its parent merges; regenerate generated files after each main merge.
| Step | What | Depends on |
|---|---|---|
| N0 | These spec amendments (docs only) | None |
| L1 to L3 | #3932, #3938, #3941 as amended: flags registered in the feature registry and read through IFeatureDecisions; indexes as pre-deploy migrations; workers idle while their flag is off; run:e2e-full |
N0, then in order |
| N1 | C15 v2 core: kind registry, generic Source, NotificationView, one capture service |
L3 |
| N2 | Per-recipient state (DismissedAtUtc, resolution fields) and the new unread rule; reads independent of capture |
N1 |
| N3, N4 | Expanding card, calm header and dismiss UI; compact bell panel (owner screen acceptance) | N2, then N3 |
| Staging switch-on | In-app only, with Chris's recorded approval (decided 26 October 2026, D46 B) | N4 |
| L4, L5, N5 | #3942 and #3943 as amended (tolerant preferences, mute, pause, digest claim skips dismissed and resolved, decided 26 October 2026, D49); daily digest preview | L3 and N1; L4; L5 |
| N6, N7 | Recorded fan-out; featureRelease kind (6G.N) |
N1; N3 and N6 |
| L6 to L9 | #3944, #3965, #3945, #3947, each behind its own flag | L5, then in order |
Previous merge order, kept for history:
Merge order (D1-09, approved by Chris on 3 October as recommended; register §1.13): step 0 the ownership fix #3964 (done: merged on 3 October as
85e6facf7, D1-01 carried out; it editsProjectController.UpdateProject, which #3941 wraps) → #3932 (regenerated generated files on currentmain, full checks on the retargeted head,run:e2e-full, human approval) → #3938 → #3941 (rebased on step 0, with a test that a refused owner-reserved grant captures nothing) → #3942 (with tolerant preferences) → #3943 → #3944 → #3965 (restacked if the stack owner agrees; otherwise as in §2's merge route) → #3945 → #3947 last (native PDF tools moved out of the API image, or the isolation review finished).
#3965 route. Decided 26 October 2026 (D45 B, §8.1): after #3944 merges, a new PR from main re-applies #3965's ten commits by cherry-pick, and #3965 is closed with a link to it. Until #3944 merges, #3965 stays where it is.
Ownership: the notification programme keeps capture, inbox, email and digests. StudyConversation
moves to L6 at R4a; study issues and checked PDFs move to the Study Management and PDF programmes. An
ADR "Saved notification capture and delivery" lands with #3932 or with C15 v2, and the MongoDB reference
lists the new collections.
6. Open items for Chris¶
24 October 2026: the inbox restart's open items (D45 to D50) were all answered on 26 October 2026. They are in §8.1.
- Q-10: decided by Chris on 3 October; implemented in #3965 (ten commits pushed, ready for review; includes the legacy cross-stage reconciler refusal; waits for the stack, D1-09).
- Batch D: D3-01 (screen timing), D3-07 ("My work"), D3-21 (enablement),
D3-22 (project name in email), D3-23 (auto-resolve), D3-24 (per-project email mute), D3-25
(conversations as audit record). D1-09 (merge order) was decided on 3 October; restacking #3965
onto #3944 still needs the stack owner's agreement.
Owner session, 5 October 2026: none of these is an open owner question now. D3-07 is Decided
(U3); D3-22 is Decided-amended (O2); D3-24 and D3-25 are Decided (O2); D3-01, D3-21 and D3-23 are
Brief items (D3-01 under
T-UX-00; D3-21 and D3-23 underT-AC-00in the implementation tracker). The stack owner's agreement to restack #3965 is still outstanding (A-33). - Q-20: narrowed to outdated annotations someone else caused (
PROPOSAL). Owner session: Q-20 is Decided (Batch B): a reviewer's own edit gives an in-form warning only; a change by someone else gives one deduplicated in-app notice per recipient and cause (RD-R28). - Untracked items for the notification programme (not in #3950): retention and erasure for
preferences, the delivery ledger, digests, posts and PDF bytes (E32); fan-out limits; unsubscribe; the
notificationEmaildependency; the delivery halt; per-project admission; idle workers; route guards; duplicate invitation email (NS-25).
7. Owner-session decisions on communications (5 October 2026)¶
Source: the consolidation §6 and §7, the session register entries D3-21 to D3-25, and the condensed packages (O2 and its 4 October amendment). The rules belong to the ACD spec; this section records how they change the notification plan. Every item is planning approval only. Notification delivery remains unauthorised, and no flag, project admission or email category is enabled by anything here.
| ID | Status | What it means for the notification programme | Where |
|---|---|---|---|
| D3-21 | Brief item (T-AC-00) |
Chris approves each environment (preview, staging, production) and each kind family separately, recorded in the flag audit. Pilot projects are admitted first through a CanonicalEnrolment scope; broader admission waits for a pilot exit review. Outside production all email goes to Mailpit. An operator delivery pause (global and per family) halts dispatch without cancelling it. notificationEmail declares its dependency on notificationInbox, and inbox reads stay available whenever saved items exist. No evidence relies on runtime overrides while #3975 is open |
ACD spec §3.9; ACD-R30, ACD-R31; ACD-AE26, ACD-AE27; §5 G-NOTIF |
| D3-22 | Decided-amended (O2, 4 October 2026; OS-A26) | Emails and digests may include project and study context, including study titles. Aliases and all other content respect the recipient's permissions and the configured blinding of the relevant form or profile. Answers and free text are configurable per project, not universally omitted (PROPOSAL default: off). Authorisation and blinding are checked when each email is prepared for delivery. Sent email cannot be recalled by later access removal |
ACD spec §3.5, §4.9; ACD-R24 to ACD-R27; ACD-AE21, ACD-AE22; §3 content policy |
| D3-23 | Brief item (T-AC-00) |
When a workflow item resolves in its feature aggregate, a resolution intent marks every related notice resolved with a reason and a resolver reference shaped by disclosure. Resolved notices leave the unread count and stay in history. Opening a notice always shows current state. Read state still never resolves a workflow | ACD spec §3.7, §4.10; ACD-R32; ACD-AE24 |
| D3-24 | Decided (O2) | A person can mute email for one project. Inbox rows are still captured, and My work and the global badge are unaffected (D3-07) | ACD spec §3.6; ACD-R28; ACD-AE23 |
| D3-25 | Decided (O2) | Reconciliation conversations are retained as permissioned project audit records while the project exists, including while it is in the reversible deleted state, with no TTL. Export needs the audit or export capability and follows the identity-disclosure policy. Conversation text never appears in candidate-answer exports or agreement statistics; "questioned in reconciliation" exposure markers stay | ACD spec §3.8; ACD-R29; ACD-AE25 |
Consequences for the stack and its follow-ups (PROPOSALs for the notification programme).
- The content policy needs a small versioned store read by both email processors after the resolver and the disclosure hook (ACD spec §3.5). It belongs with C15 v2's disclosure hook (N6, E73).
- The per-project mute extends
NotificationEmailPreferenceswith muted project IDs and the time each was muted (ACD spec §3.6). It lands with the tolerant preferences change (N1) so category skew and mute ship together. InboxNotificationgainsResolvedAtUtcand a resolution sub-document (ACD spec §3.7); the domain model already reservesResolvedAtUtcfor D3-23. 24 October 2026: it also gainsDismissedAtUtcfor personal dismiss (PROPOSALfield; ACD spec §3.7).- Conversation retention settles the conversation part of E32. Retention for preferences, the delivery ledger, digests, issue posts and PDF bytes is still untracked (§6).
- Reversible project deletion stops notice capture for a deleted project and releases its places
(ACD-R14). Existing notices leave unread counts and show "Project deleted" while it is deleted
(
decided 24 October 2026: owner decision 1; the notice stays in history as "no longer available" and leaves the unread count and the badge).PROPOSAL, AC ambiguity B8 - Feature queues stay the source of truth. Every release still passes its acceptance tests with every notification flag off (ACD-AE30).
8. Inbox restart: owner decisions of 24 October 2026¶
Source: Chris's decisions in the notifications prototype session of 24 October 2026 (artifact status
and coverage matrix in the handover pack, outside the repository) and the implementation plan at
/home/chris/workspace/syrf/handover/notifications-implementation-plan-2026-10-24.md, §4. They apply to the notification stack
and amend C15 v2 (contracts), the ACD spec and the UX strategy. Planning approval only:
nothing here enables a flag, admits a project or sends an email.
8.1 Decided¶
| # | Decision | Where it lands |
|---|---|---|
| 1 | Notices the reader can no longer open leave the unread count and the badge. They stay in history as "no longer available". | §1.3 item 5; §7; C15 v2 "Per-recipient state"; ACD spec §3.7, §4.10 |
| 2 | Personal dismiss with Undo, Clear all, a Dismissed filter and Restore. A dismissed notice stays in history, leaves the unread count and never resolves work. Digests skip it at claim time (D49, decided 26 October 2026). | C15 v2 "Per-recipient state", "Enablement"; ACD spec §3.7, §4.9 |
| 3 | One expanding notice card everywhere. It replaces the Inbox detail pane. /inbox/:id links keep working by opening the Inbox with that card expanded. |
C15 v2 "Source and view"; §5 stack screens |
| 4 | Per-kind templates shaped at read time, with inline actions and blinded aliases. An unavailable notice shows the kind only. | C15 v2 "Source and view" |
| 5 | A compact bell panel. | §5 stack screens |
| 6 | A calm Inbox header: tabs All, Unread, Resolved and Dismissed, search, one Filters panel and chips. | §5 stack screens |
| 7 | A daily digest preview, and the same design for dispatched digests. | C15 v2 "Enablement"; ACD spec §4.9 |
| D42 = C | Release notices are one more inbox kind, featureRelease (6G.N). |
§3; C15 v2 "Kind registry"; remaining delivery plan |
| D45 = B | After #3944 merges, open a new PR from main that re-applies #3965's ten commits by cherry-pick, and close #3965 with a link to it (decided 26 October 2026). Effect: private conversations no longer wait for the PDF isolation review. |
§5 stack order; "#3965 route" |
| D46 = B | Staging switch-on is in-app only, with Chris's recorded approval. Reads independent of capture ship in N2. Email stays off and paused. Per-project admission waits until before production (decided 26 October 2026). | §5 staging switch-on; N2 |
| D47 = A | Release notices are created from the What's New step (MaterialiseWhatsNew), with expansion stopping on AnnouncementCancelled (decided 26 October 2026). A stopped release shows the notice as "no longer available" (decision 1). |
featureRelease (N6, N7); §3 |
| D48 = A | Restore of a dismissed release notice brings back the inbox row only. What's New stays dismissed (decided 26 October 2026). No change to the release contract. | featureRelease (N7); RuntimeFeatureDiscoveryStore unchanged |
| D49 | Decided 26 October 2026, assumption by assumption. (1) Changed: opening a notice marks it read (Chris's note: "1) opening should mark it as read"). Read never resolves work. (2) Release notices are in-app only. (3) Digests skip resolved and dismissed items at claim time. Assumptions 2 and 3 are recorded as confirmed on the lead's reading of Chris's note, pending his correction. | C15 v2 "Per-recipient state" and "Enablement"; ACD spec §3.7, §4.9; UX strategy §7.9; AC C15-T14, C15-T16. Implementation: the N3 expanding card marks the row read when it opens (L1's /inbox/:id page is replaced by N3) |
| D50 = A | Registry type Release, lifecycle Developing, What's New quiet until the inbox is released, with user-guide content (decided 26 October 2026). #3932 already registers it as Release. |
notificationInbox registry entry; 6G.N |
The seen and dismissed store that release notices share with What's New is RuntimeFeatureDiscoveryStore
(collection runtimeFeatureDiscoveryState). "C15 §1.3 #5" in the prototype pack means §1.3 item 5 of
this page.
8.2 Pending the owner¶
No decisions remain pending. D45 to D50 are all decided (§8.1).