Temporary planning review record. The report below is reproduced verbatim as returned by the independent read-only reviewer (Plan agent, Opus model, launched 3 October 2026 about 15:00 BST at Chris's request to review in-flight programmes and their integration with the plan). Only this front matter and note were added. #3965's local commits were not visible to the reviewer (it saw the empty GitHub branch). Line numbers refer to the package as it stood when the reviewer read it. Resolutions are in the round-2 resolution matrix.
NS: notification service review (round 2, read-only)¶
Evidence snapshot. I read live gh state between 14:40 and 15:04 BST on 3 October 2026. main is at 0f5c61073, 37 commits after the plan's c59d9d0f1 baseline; none of those commits touch notifications. I read the stack code in the clean top-of-stack worktree at ae3c6749d, which contains #3932 to #3947. I saw #3965 only through gh, because another agent is editing its worktree. I wrote, ran and sent nothing.
Path prefixes (all absolute):
- PKG = /home/chris/workspace/syrf/pr/pr3617.research-screening-as-specialised-annotation-gxgahs/docs/planning/integrated-review-plan-2026-10/
- LEDGER = /home/chris/workspace/syrf/pr/pr3617.research-screening-as-specialised-annotation-gxgahs/docs/planning/review-form-owner-decisions-2026-10-02.md
- LC = /home/chris/workspace/syrf/pr/pr3617.research-screening-as-specialised-annotation-gxgahs/docs/planning/review-lifecycle-gold-settings-proposal-2026-10-03.md
- STACK = /home/chris/workspace/syrf/pr/pr3947.checked-pdf-proposals-and-explicit-administrator-a-r6bdl2/
- MAIN = /home/chris/workspace/syrf/main/
Stack files cited by name (all under STACK):
- SourceInboxCapture.cs, InboxNotificationRepository.cs, StudyConversationRepository.cs, StudyIssueRepository.cs, CheckedPdfProposalRepository.cs, DataExportJobRepository.cs, NotificationEmailDeliveryRepository.cs: src/libs/project-management/SyRF.ProjectManagement.Mongo.Data/Repositories/
- InboxNotification.cs: src/libs/project-management/SyRF.ProjectManagement.Core/Model/NotificationAggregate/
- NotificationEmailPreferences.cs: src/libs/project-management/SyRF.ProjectManagement.Core/Model/NotificationEmailPreferencesAggregate/
- StudyConversation.cs: src/libs/project-management/SyRF.ProjectManagement.Core/Model/StudyConversationAggregate/
- StudyIssue.cs: src/libs/project-management/SyRF.ProjectManagement.Core/Model/StudyIssueAggregate/
- StageNotificationSnapshot.cs: src/libs/project-management/SyRF.ProjectManagement.Core/Services/Notifications/
- Project.cs: src/libs/project-management/SyRF.ProjectManagement.Core/Model/ProjectAggregate/
- NotificationDetailResolver.cs, NotificationEmailProcessor.cs, NotificationEmailWorker.cs, NotificationDigestProcessor.cs, NotificationDigestWorker.cs, StudyConversationAccess.cs, StudyIssueAccess.cs: src/services/api/SyRF.API.Endpoint/Services/Notifications/
- NotificationsController.cs, NotificationEmailPreferencesController.cs, StudyConversationsController.cs: src/services/api/SyRF.API.Endpoint/Controllers/
- InboxInvalidationWorker.cs: src/services/api/SyRF.API.Endpoint/SignalR/
- Program.cs, Dockerfile: src/services/api/SyRF.API.Endpoint/
- RuntimeFeatureFlagCatalog.cs: src/services/api/SyRF.API.Endpoint/RuntimeFeatureFlags/
- env-mapping.yaml: src/charts/syrf-common/
- notification-inbox.component.html, notification-inbox.component.scss, notification-inbox-button.component.ts, notification-email-preferences.component.ts: src/services/web/src/app/notifications/
- app.routes.ts: src/services/web/src/app/
- technical-plan.md, daily-digest-plan.md, checked-pdf-plan.md: docs/features/notification-inbox/
1. Verdict¶
The plan's notification stance is sound: consume the existing stack, keep workflow state in feature aggregates, and never let confirmed obligations depend on notifications. Its facts about the stack still hold at today's heads.
Two things are wrong with it. First, it treats a nine-PR stack that is unmerged, conflicting at its base, reviewed only by bot comments and never run through CI E2E as a stable platform. Second, it adopts that stack's extension model wholesale. In that model one kind is one email category, the resolver is a central switch, every source type adds a field, and inline per-recipient writes happen inside the source transaction. That model was built for eight fixed kinds, and the plan needs about twenty more. As written: - C15 cannot express the plan's own deferred, lazy and time-driven events. - Every new kind breaks preference saves, including opt-out, whenever the browser and API versions differ. - Every lane must edit the same notification-owned files. - Enablement is environment-wide and, for email, survives turning the flag off. So notifications cannot be piloted per project under Chris's approval. - The Q-10 privacy fix (#3965) has no code and is queued behind PDF-scanning work.
The five most important changes: 1. Freeze a C15 v2 capture contract at F1. It covers a kind registry with grouped email categories, deterministic occurrence identity, inline and recorded-fan-out capture modes, a generic source reference, typed availability and a channel-aware disclosure hook. The notification programme implements it after the base merges and before the first review-workflow kind. 2. Before #3942 merges, make preferences tolerant of unknown or missing categories, and separate categories from kinds. 3. Restack #3965 onto #3944 and merge both before #3945 and #3947. Wire its "questioned" exposure record into C3, R5c and R6. 4. Add enablement controls behind a G-NOTIF gate that Chris approves per environment: per-project notification admission, an operator delivery halt, and inbox reads that don't depend on capture. 5. Make the four feature queues flag-independent alerts (a global badge, a cross-project "My work" view and an admin banner). Add acceptance criteria for the notifications-on path.
Nothing here is a Blocker for G0. Four of these changes are cheap only if made before the relevant stack PR merges.
2. Current state¶
| PR / component | State | Evidence | Known gaps |
|---|---|---|---|
main 0f5c61073 |
No inbox: no file mentions InboxNotification. An unused legacy Notification entity exists. Email goes through SES in production and SMTP to Mailpit in staging and preview. Three outbox patterns already exist: FEAT-024's statistics notification outbox (#3107), the claim-revocation outbox (#3736) and Identity's recovery-email outbox. Runtime flag overrides are disabled in production |
MAIN/src/libs/project-management/SyRF.ProjectManagement.Core/Model/InvestigatorAggregate/Notification.cs:6; MAIN/src/services/api/SyRF.API.Endpoint/Models/ApiEmailTransportRegistration.cs:23-41; /home/chris/workspace/cluster-gitops/syrf/environments/staging/api/values.yaml:68-75; MAIN/docs/how-to/manage-feature-flags.md:188-206 |
— |
| #3932 inbox and export capture | OPEN, CONFLICTING (DIRTY), REVIEW_REQUIRED. Head 2ddd96fb0, +2,257/−188, 51 files. Merge-base 12e17b92d; main is 65 commits ahead |
gh pr view 3932. Conflicts are only in generated files: api-client.generated.ts, swagger.json, .generated-checksums.json, consumer-manifest.json |
No human approval. E2E skipped. Inbox reads are gated by the capture flag. Action labels are inferred in the browser |
| #3938 import and invitation | OPEN, mergeable into #3932's branch. 180f0d1bb, +693/−23 |
gh |
Opted-in users get duplicate invitation emails (NS-25) |
| #3941 access and workload | OPEN. eb72c886d, +915/−67 |
StageNotificationSnapshot.cs:18-41 |
SourceIds are random. Uses the legacy ACL and stage targets. Wraps ProjectController.UpdateProject, which ownership PR #3969 (draft) also edits. Some changes are unflagged (NS-16) |
| #3942 preferences and immediate email | OPEN. 75f89b33d, +1,877/−109 |
NotificationEmailPreferences.cs:15-22; preferences controller has no flag |
Categories must match an exact set. No unsubscribe, no halt. notificationEmail declares no dependency |
| #3943 daily digests | OPEN. d75fd81b9, +1,010/−30 |
NotificationDigestProcessor.cs:50-60 |
Titles only, ungrouped, capped at 100 a day |
| #3944 reconciliation conversations | OPEN. 59ab32e7b, +2,222/−24 |
StudyConversationAccess.cs:46,52; StudyConversationRepository.cs:66-68 |
Conflicts with Q-10. Reads legacy sessions. Labels differ between picker and thread |
| #3945 study issues | OPEN. 25b364da3, +2,256/−28 |
StudyIssueRepository.cs:69-89; StudyIssueAccess.cs:22,41-43 |
Writes to Study. Recipients are Administrator-group members |
| #3947 checked PDFs | OPEN. ae3c6749d, +3,009/−16 |
Dockerfile:7; CheckedPdfProposalRepository.cs:131,150; checked-pdf-plan.md:20-23,119-124 |
Adds native tools to the API image on merge. Needs a clamd scanner and a native-parser isolation review. Writes the Study PDF path |
| #3965 private conversations | OPEN draft, 0 files (commit 512c9f974, "chore: initialize"). Based on #3947's branch |
gh pr view 3965 |
No code. RA5 exclusion missing from its change list |
| #3950 follow-ups | OPEN, 13 items | gh issue view 3950 |
Doesn't track retention, fan-out limits, unsubscribe, the email flag dependency, a halt or per-project admission |
| Checks and reviews (all eight) | 29 success, 34 skipped, 1 neutral. E2E jobs skipped (no run:e2e-* label). The latest-head Claude reviews are 17–39 s comment reviews of main-merge deltas |
statusCheckRollup; PR comments; technical-plan.md:143-149 (product CI runs only while a PR is temporarily retargeted to main) |
The eight new E2E specs (e2e/tests/notification-*.spec.ts and others) have never run in CI |
| Flags and workers | Three flags, default off, environment-wide, API and web only. studyAttention depends on notificationInbox. The change-stream watcher, email worker (every 15 s) and digest worker (every 15 s) start in every API replica regardless of flags |
env-mapping.yaml diff; RuntimeFeatureFlagCatalog.cs diff; Program.cs:217,232,235; NotificationEmailWorker.cs:34-35; NotificationDigestWorker.cs:25,33 |
Accepted email continues after the flag is turned off |
| Docs | Feature docs are Draft with ZenHub "TBD"; a user guide was added | STACK/docs/features/notification-inbox/ |
No ADR. mongodb-reference.md doesn't list the new collections |
3. Findings¶
| ID | Severity | Location | Finding | Evidence | Recommended change |
|---|---|---|---|---|---|
| NS-01 | Major | PKG/contracts.md C15 l.534-536; PKG/notifications-integration.md §1.3 item 1 l.61-64, §4 item 1 l.157-161; PKG/domain-model.md §5 l.174, 177; PKG/contracts.md C4 l.219-223; PKG/open-questions-and-assumptions.md E30 l.132 |
C15 requires capture "in the same transaction as the domain change", and §1.3 forbids any outbox. Several of the plan's own events have no such transaction: • publication notices sit in phase 2 (eventual), or have no transaction at all when the policy is "evaluated lazily on read"; • outdated-flag fan-out is eventual; • assignment "about to expire"/"expired" and LC1 reminders are time-driven, with no user command; • R6 adoption cutover notices go to whole projects. Implementers must either break the stack's guard or invent a mechanism per lane. |
SourceInboxCapture.cs:58-60 throws outside an active transaction. domain-model.md:177 ("Phase 2 … notices once per recipient"). contracts.md:220-221 (lazy option). notifications-integration.md:142 (expiry rows). E30 omits FEAT-024's existing notification outbox (#3107). |
Rewrite C15 with two capture modes. Inline: in the active source transaction, with a bounded recipient count. Recorded fan-out: the source transaction persists one durable intent (the publish operation's frozen manifest, or a NotificationFanOut record); a leased, idempotent API worker writes per-recipient rows in bounded batches and records progress. Time-driven notices are domain transitions run by a scheduler that writes a marker on the aggregate (e.g. ReconciliationAssignment.ExpiryWarningIssuedAt) and captures inline. Replace "none should be added" with: "no second notification store; durable intents are part of C15; in-memory outboxes stay forbidden" (this is the stack's own principle, technical-plan.md:70-71). Settle in E30 at F1, listing all four existing post-commit mechanisms. |
| NS-02 | Major | PKG/notifications-integration.md §1.3 item 3 l.70-74, §4 item 5 l.169-170 |
"A Kind is also an email category key", combined with exact-set validation, makes every new kind a breaking change. During a rolling deploy or image rollback, whichever side (browser or API) has a different category list rejects every preference save with 400, including opt-out. About twenty new kinds would also mean about 28 toggles. The preferences API is unflagged, so preference documents can exist in production as soon as #3942 deploys. | NotificationEmailPreferences.cs:15-16,22 (Categories.Count == CategoryNames.Length). NotificationEmailPreferencesController.cs:34-41 (no flag; 400). notification-email-preferences.component.ts:40-49,130-139 (sends only its hard-coded list). NotificationEmailDeliveryRepository.cs:83-84 (dispatch consent keyed by Categories.{Kind}). technical-plan.md:206-209 |
Before #3942 merges: • accept unknown keys (keep and ignore them) and treat missing keys as "off"; • make the client send back keys it doesn't render; • add a kind→email-category map: the eight existing kinds map to themselves, and review-workflow kinds map to at most six new categories (§5.3); • make dispatch, discovery and digests look up the category; • add a version-skew test matrix. If #3942 merges unchanged, the tolerant validator must still deploy before the first new category (N-1). |
| NS-03 | Major | PKG/notifications-integration.md §1.3 item 3, §4 items 1, 3, 5; PKG/contracts.md C15; PKG/integrated-plan.md §7 rule 1 l.958-960 |
The extension points are per-kind code in shared files owned by the notification programme: • a central resolver switch; • a nullable field on InboxNotification per source type;• typed result fields on the DTO; • a browser-side action-label chain whose fallback labels any new kind "Open conversation"; • a capture method that rejects all but two kinds; • five separate inbox writers with different identity rules. Every lane adding a kind edits the same files, which works against contracts-first parallel work. Access is also checked against the captured StageId, which fails for RE4 tasks reachable through several stages. |
NotificationDetailResolver.cs:18-28. InboxNotification.cs (StageId, ConversationId, StudyIssueId, StudyId, WorkloadChange). NotificationsController.cs:84-90. notification-inbox.component.html:70-81. SourceInboxCapture.cs:43-44. Writers: SourceInboxCapture.cs:68, DataExportJobRepository.cs:133, StudyConversationRepository.cs:66-68, StudyIssueRepository.cs:88-89, CheckedPdfProposalRepository.cs:150 |
Add a kind registry: each feature registers, through DI, its kind, email category, scope (legacy, canonical or both), admission flag, inline recipient limit and resolver. Add a generic Source sub-document (type, IDs, stage list, task ID); server-provided action label, context lines, typed availability and workflow state; and one capture service for all writers. Keep the typed fields for the eight existing kinds, so no data migration is needed (Entity extra elements keep older binaries tolerant). A registry test fails the build when a kind lacks a category, resolver, label or disclosure fixtures. Specify at F1; land after the base merges and before the first review-workflow kind. |
| NS-04 | Major | PKG/notifications-integration.md §1.3 items 7-8 l.81-88, §5 l.203; PKG/integrated-plan.md §5.11 l.746, §6.2; PKG/open-questions-and-assumptions.md Q-25 l.52, A-23 l.205; PKG/decision-register.md l.175 |
Enablement can't be scoped or fully reversed, so "no notifications reach real project users before Chris approves" rests on discipline alone: (a) The flags are environment-wide. Turning on notificationInbox for one production pilot captures exports, imports, invitations, access and workload changes for every project.(b) Turning notificationEmail off stops new admission, but accepted immediate and digest mail keeps sending; there is no pause.© Turning notificationInbox off returns 404 for every inbox read, which breaks links already emailed.(d) notificationEmail declares no dependency on notificationInbox.(e) In staging and preview, any SyRF administrator can override these flags at runtime. |
env-mapping.yaml (global flags, services: [api]). NotificationEmailWorker.cs:34-35. NotificationDigestWorker.cs:33. daily-digest-plan.md:94-97. NotificationsController.cs:25. MAIN/docs/how-to/manage-feature-flags.md:188-206 |
Before any enablement outside e2e and Mailpit: • per-project notification admission (an R0 admission scope, or an interim registry until R0 exists), checked at capture for every kind; • an operator delivery halt that pauses dispatch without cancelling it; • inbox reads available whenever saved items exist, regardless of capture admission; • the declared email→inbox dependency; • a G-NOTIF gate in plan §6.2: Chris approves each environment and kind family, including staging and preview overrides, recorded in the flag audit comment. |
| NS-05 | Major | PKG/notifications-integration.md §2 l.111-120; PKG/decision-register.md l.189; PKG/integrated-plan.md §8 l.988, §12 l.1090-1091 |
#3965 has no code and is stacked on #3947, so the conversation privacy fix waits behind study issues and checked PDFs. #3947 needs a private scanner and an isolation review before enablement, and its merge changes the production API image. Meanwhile studyAttention admits conversations together with study issues: enabling it to test #3945/#3947 also enables the non-private conversations that VS1 forbids. #3965's change list and the decision register both drop the RA5 exclusion that §2 says is required before enabling. |
gh pr view 3965 (0 files; base is #3947's branch). env-mapping.yaml studyAttention description. checked-pdf-plan.md:20-23,119-124. Dockerfile:7. notifications-integration.md:113-114 vs decision-register.md:189 |
Restack #3965 onto #3944, or fold it in before #3944 merges. Merge order: #3944 → #3965 → #3945 → #3947. Name the new flag (e.g. reconciliationConversations), declare its inbox dependency and rewrite studyAttention's description. Reconciler eligibility:• legacy: refused if the reconciler has any non-reconciliation session on the study in any stage whose questions overlap; • from R4a: refused if they have any session on that study × form. Record the RA5 exclusion as an R4a precondition (Q-N9). |
| NS-06 | Major | PKG/contracts.md C3 l.159-170; PKG/integrated-plan.md R5c l.662-671; PKG/migration-adoption-rollback.md l.110 |
Q-10's exposure record lives only inside StudyConversation. C3 knows only accepted-revision exposure, and R5c separates independent from informed answers through VS2 alone. A candidate who changes an answer after a reconciler's question, which may paraphrase other candidates' answers, would therefore count as independent in agreement statistics. Exports and R6 manifests don't carry the record either. |
decision-register.md:189 ("exposure record"); notifications-integration.md:104; contracts.md:163 |
Add a C3 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. Fixture: a questioned reviewer saves a new version and agreement classifies it as informed. Freeze with C3 at F1; #3965 stores the record so it can be looked up by session. |
| NS-07 | Major (the A-01 resolution is incomplete) | PKG/notifications-integration.md §4.1 l.181-192; PKG/ui-coverage-comparison.md §4; PKG/acceptance-criteria.md AC-R3c-02 l.245, AC-R4a-10 l.275, AC-R4b-03 l.295 |
The queues carry the confirmed obligations when notifications are off, but the plan only says items appear in a queue. Nothing surfaces them: the IA has no place for the queues, and there is no cross-project view and no badge. LC1 says "alert the project admin and obtain admin confirmation"; QY6 says each raiser "receives" their outcome. A passive per-project queue does neither for someone who doesn't visit that project, and a reviewer whose Fix waits on LC1 approval may wait indefinitely. | LC:22-24; LEDGER:221-224; MAIN/src/services/web/src/app/project-index/project-index.component.html:74-82 (only a "Pending Projects" tab) |
Add a flag-independent "My work" surface to C17: • a global navigation badge whose counts are 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. Acceptance: with every notification flag off, a user reaches an assignment in an unopened project from the project index in one step, and an admin with a pending request sees the badge without opening the project. Add UI validation U30. |
| NS-08 | Major | PKG/migration-adoption-rollback.md §1.4 l.32-46; PKG/integrated-plan.md P1/P2 l.689-690 |
The writer inventory misses two Study writers added by the stack. Study-issue acceptance rewrites Title, Abstract, Year, DOI or Url; checked-PDF approval rewrites the PDF path. A DOI or title change after import never re-runs P2's synchronous DOI/PMID dedup, and an approved PDF is a retrieval event P1 should record. The inventory lists #3944 as a reader only. | StudyIssueRepository.cs:69-84 (Study update at :78); CheckedPdfProposalRepository.cs:131; migration-adoption-rollback.md:49 |
Add both writers to 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. |
| NS-09 | Major | PKG/integrated-plan.md R1c l.358-359, §5.11 l.746, §6.3 l.909; PKG/acceptance-criteria.md AC-R1c-04 l.136; PKG/notifications-integration.md §3 l.152 |
#3941 is legacy-specific. It decides "effective Review access" through the legacy ACL path without the evaluator. It ignores Reconcile grants (its capture rejects any other kind). It computes workload from stage.WorkloadShares and stage.SessionCountTarget, which become legacy-only for canonical forms. After the authorization cutover, or in canonical projects, its notices will disagree with enforcement or not appear. Separately, AC-R1c-04 requires notices "through #3941's capture and no other path". That makes the stack a hard R1c dependency, yet X-NOTIF says "notices only" and the graph links X-NOTIF only to R4b. |
StageNotificationSnapshot.cs:18-26; SourceInboxCapture.cs:43-44; integrated-plan.md:411-413 |
(a) Read effective grants through the active decision path, with a parity test, and add Reconcile grants. (b) For canonical forms, derive workload notices from the form target and the AL1 plan (C7), and suppress the stage-based capture. © Make "#3932–#3941 merged" an explicit R1c entry criterion, or make AC-R1c-04 conditional. (d) Add dashed X-NOTIF edges to R1c, R2c, R3c and R4a. |
| NS-10 | Major | PKG/ui-coverage-comparison.md l.107; PKG/acceptance-criteria.md §3; PKG/decision-register.md §1.11 UI1 |
The plan "retains the notification stack's UI", but UI1 requires every new screen to meet the Material 3 standard, and these screens are new to SyRF. The inbox is a hand-rolled list with currentColor dividers, a plain count beside the bell rather than a badge, text-only loading and empty states, and no project or context line. It doesn't evidently meet UI-2 or UI-7, and it becomes an "updated screen" as soon as a review-workflow kind lands. |
notification-inbox.component.html:93-110; notification-inbox.component.scss:19; notification-inbox-button.component.ts:11-17 |
Replace "Retain" with: the stack's screens pass UI-1 to UI-8 before testers see them (inbox, preferences) and before production (conversation, issue, PDF). Schedule a Material 3 inbox redesign PR before R2c. Timing is Q-N5. |
| NS-11 | Major | PKG/acceptance-criteria.md §2, §4; PKG/integrated-plan.md §10 l.1046 |
Every release must pass with notification flags off, but no criterion tests the flags-on path. The risk table's "fixtures for every event" and §4 item 8's tests aren't acceptance criteria. A release could add a kind that leaks a raiser's identity, duplicates on retry or floods inboxes, and still pass. | acceptance-criteria.md:136,195,295; notifications-integration.md:177-178 |
Add the C15 criteria in §5.4 to the release checklist for any release that adds a kind, and strengthen the listed release criteria. |
| NS-12 | Minor | PKG/contracts.md C15 l.539-541; PKG/notifications-integration.md §1.3 item 2, §4 item 1 |
C15 says to give each occurrence its own SourceId "as reviewAccessGranted already does". That kind uses Guid.NewGuid(); its idempotency comes from comparing before and after snapshots. Copied into a resumable publication batch, random IDs duplicate notices on resume. §4 also folds the recipient into "the identifier", mixing up the SourceId (shared by all recipients) with the row ID. |
StageNotificationSnapshot.cs:41; SourceInboxCapture.cs:68. Deterministic models: StudyConversation.cs:20-25, StudyIssue.cs:24-27 |
SourceId = SHA-256(kind, source type, source ID, occurrence key), the same for every recipient. Row ID = SHA-256(SourceId, recipient). The occurrence key comes from durable identity (command ID, aggregate version, transition, publish operation ID). Capture uses upsert with $setOnInsert, not InsertOne. |
| NS-13 | Minor | PKG/notifications-integration.md §5 l.202; PKG/integrated-plan.md §10 l.1057 |
Flood control is deferred to "#3950 fan-out limits", but the stack has: • no mark-all-read; • a list showing only title and date; • unavailable items that stay unread forever; • digests of up to 100 bare titles a day, with overflow rolling into later days; • a hint on every write followed by every tab refetching at once, with authority re-checked per item; • one notice per repeated publication of the same form. |
NotificationsController.cs:37-76; notification-inbox.component.html:93-110; NotificationDigestProcessor.cs:50-60; daily-digest-plan.md:14-18; InboxInvalidationWorker.cs:22-29 |
Before R2c: • mark-all-read and project/kind filters; • a context line from the resolver; • "hide unavailable"; • an unread count that excludes resolved items (Q-N3); • 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. |
| NS-14 | Minor | PKG/open-questions-and-assumptions.md Q-20 l.61; PKG/acceptance-criteria.md AC-R2d-08 l.210 |
Q-20 frames "contains outdated annotations" as a fan-out risk. But candidate answers belong to their reviewer (C2), so the flagged sessions are the changer's own. The ledger already requires an in-form alert to them, and C15 excludes the actor. A notice matters only when someone else caused the change. | contracts.md:138-149; LEDGER:465-468; notifications-integration.md:163-164 |
Narrow Q-20 (Q-N7): 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, a publication autoUpdate, an adoption remap or a shared-gold revision. |
| NS-15 | Minor | PKG/notifications-integration.md §3 l.130-153 |
Missing catalogue rows: • a phase-2 completed or stalled notice for the publishing admin; • canonical workload changes (form target, AL1 plan); • R6 cutover, pilot admission or removal, and a pilot becoming read-only (U28); • a kind for Reconcile grants. The two R2c rows would be two kinds for one publication, breaching AC-R2c-07's "at most one notice". Banners are SignalR, not C15. |
notifications-integration.md:132-134; acceptance-criteria.md:195 |
Add the rows (§5.3). Make publication one kind whose per-recipient effects are worked out at read time. State that live banners use the existing per-user SignalR groups. |
| NS-16 | Minor | PKG/notifications-integration.md §1.2 l.40-51; PKG/source-status-inventory.md l.344 |
The plan treats the stack as dark, but merging it 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. |
NotificationEmailPreferencesController.cs:13-41; app.routes.ts:70-108; Project.cs:1644; technical-plan.md:227-232; Program.cs:217,232,235; Dockerfile:7 |
List these as "live on merge". Ask the owner 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. |
| NS-17 | Minor | PKG/integrated-plan.md §8 l.988, §5.11 l.746 |
The plan doesn't state the merge risk: • nine stacked PRs, about 14,200 added lines including regenerated clients; • a base 65 commits behind main, conflicting in generated files;• CI that runs only while a PR is temporarily retargeted to main;• E2E never run in CI, and no human approval; • #3941 rewrites ProjectController.UpdateProject, which #3969 also changes.There is also no ADR and no MongoDB reference entry for the new collections. |
gh states and checks; technical-plan.md:143-149; #3969 file list; no docs/decisions change |
Add the merge protocol in §4.3 and an "X-NOTIF met" definition. Write an ADR, "Saved notification capture and delivery", with #3932 or with C15 v2. |
| NS-18 | Minor | PKG/contracts.md C1 l.108; PKG/notifications-integration.md §5 l.201 |
C1 says the Study summary projection serves "#3944's candidate lookup". #3944 reads full legacy session objects, which a bounded tally projection can't supply. In canonical studies it finds no candidates and refuses replies. | StudyConversationAccess.cs:46,52 |
Remove #3944 from the projection's readers. Conversations refuse canonical scopes (through R0's ownership record) until R4a binds them to ReconciliationTask. L6 owns StudyConversation from R4a. |
| NS-19 | Minor | PKG/integrated-plan.md l.236, 986, 988 |
Ownership is doubled. #3947 appears under both the PDF programme and the notification stack. L6 must rebind StudyConversation in R4a, but the notification programme owns that code. Study issues and checked PDFs are study-attention features, not notifications. |
Same lines | 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. |
| NS-20 | Minor | PKG/contracts.md C10 l.391-431 |
Study-issue recipients and deciders are Administrator-group members, not holders of a capability. After R1c, a custom group holding Edit and ViewStudies gets no reports. | StudyIssueAccess.cs:22,41-43 |
Add "receive and resolve study issues" and "approve PDF corrections" to the C10 catalogue at F1. Expand recipients by capability before R1c ships. |
| NS-21 | Minor | PKG/open-questions-and-assumptions.md E32 l.134; PKG/migration-adoption-rollback.md l.65-68 |
Erasure and retention cover "inbox items" only. The stack also stores preferences, the delivery ledger, digests (which hold notification IDs), conversation and issue posts (free text) and PDF bytes. None of these has retention. | StudyConversation.cs:15; no TTL index in any stack repository |
Extend E32 to these stores. Retention must not orphan ledger or digest references. Post text follows the annotation erasure rule. |
| NS-22 | Minor | PKG/notifications-integration.md l.202, 210-212 |
The plan says #3950 tracks retention, fan-out limits, unsubscribe, the email flag dependency and idle workers. Of these, #3950 covers only an observer-load measurement (item 2). | gh issue view 3950 |
Mark these items "untracked" and ask the notification owner to open issues for them. |
| NS-23 | Minor | PKG/domain-model.md §5 l.174-179; PKG/acceptance-criteria.md AC-R2a-19 l.172, AC-R2c-06 l.194 |
The transaction table puts unbounded inbox writes on Save and Complete (the hottest path) with no benchmark. It omits inbox rows from gold publication (QY8 notices) and query resolution, and doesn't define how publication notices are captured. | domain-model.md:174,177-178 |
Give each operation a capture mode. Save/Complete: inline, 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-24 | Minor | PKG/contracts.md C9 l.383 |
Three channels overlap: study issues (which include an "other" kind), pre-gold conversations and post-gold queries. A reviewer can dispute gold through an "other" issue and bypass the QY lifecycle. | StudyIssue.cs:23 |
After R4b, the issue form in canonical projects sends answer disputes to "Raise a query", and C9 says issues never carry answer concerns. |
| NS-25 | Note | — | A user who opts into invitation email gets two emails per invitation: the existing invitation mail and the notification mail. | technical-plan.md:123-127; NotificationEmailPreferences.cs:15 |
Remove projectInvitation from the email categories, or skip notification mail when invitation mail was sent. |
| NS-26 | Note | PKG/migration-adoption-rollback.md §5 |
Rolling back past a release that added a kind permanently suppresses that kind's pending email. Older binaries treat unknown kinds as unavailable and mark the ledger row Suppressed, a terminal state. |
NotificationDetailResolver.cs:27; NotificationEmailProcessor.cs:33-36,78 |
Ship "an unknown kind leaves the ledger row ready" one deploy before the first new kind, and note it under rollback. |
4. Recommended changes to the existing implementation¶
| # | Change | Rationale | Timing | Compatibility and migration | Risk | PR slicing | Owner |
|---|---|---|---|---|---|---|---|
| 1 | Tolerant preference validation; client sends back unknown keys; kind→category map; declare notificationEmail → notificationInbox |
NS-02, NS-04 | Before #3942 merges (at the latest, one deploy before the first new category) | None. Stored documents stay valid because existing kinds map to themselves | Low; invalid modes and time zones must still be rejected | Amend #3942 (validator plus client; category map) | Notification programme |
| 2 | Restack #3965 onto #3944: own flag, one-to-one threads, completed sessions only, exposure record looked up by session, read-only link, reconciler eligibility | NS-05, NS-06 | Before #3945 merges, and before studyAttention is enabled anywhere |
Generated flag files are regenerated; #3945 and #3947 rebase mechanically; no data exists yet | Low to medium | Retarget #3965; merge it straight after #3944 | Notification programme; L6 reviews |
| 3 | Delivery halt (pause, not cancel); inbox reads independent of capture; per-project notification admission | NS-04 | Before any enablement with real users. Admission before any production pilot that has notices | New data with safe defaults; no migration | Medium | N1: halt and read gate. N2: admission (interim registry, later an R0 scope) | Notification programme; L0 |
| 4 | Kind registry, Source sub-document, single capture service, server-side label/context/availability/state, behaviour for unknown kinds |
NS-03, NS-12, NS-26 | ADR in W0, frozen at F1; landed after the base merges and before the first review-workflow kind | Additive fields; existing kinds keep their typed fields | Medium; mitigated by the existing physical tests | N3: registry with existing kinds as handlers. N4: capture service (bulk upsert, recipient cap). N5: client uses server labels | Notification programme; L14 writes the specification |
| 5 | Recorded fan-out: durable intent plus leased expander | NS-01 | Before R2c and before R1c bulk group edits | New collection. Older binaries ignore intents, which wait until roll-forward | Medium | N6: intent, worker and resume tests | Notification programme; L2 consumes |
| 6 | Disclosure-policy hook with channel rules and an alias source (§4.2) | NS-03, NS-11 | Interface at F1 with C10; a default keeps today's behaviour. BL1 and VS1 rules before R4a and R4b kinds | Code only | Low | N7: hook and fixture harness | L8 with the notification programme |
| 7 | #3941 alignment: active decision path, Reconcile grants, canonical workload suppression | NS-09 | Before R1c (grants); before R2b and AL1 (workload) | Code; parity tests | Medium | N8 | Notification programme with the authorization and allocation owners |
| 8 | Material 3 inbox and preferences; grouped digest; flood controls; resolved and superseded state | NS-10, NS-13 | Before testers see it; flood controls before R2c | Additive ResolvedAtUtc field; new endpoints |
Low to medium | N9: inbox redesign. N10: grouped digest. N11: refetch jitter and memoisation | Notification programme; L16 design review |
| 9 | 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 |
| 10 | Conversations refuse canonical scopes; rebind to the task at R4a; RA5 exclusion; 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 |
| 11 | Merge #3947 last; move native PDF tools out of the API image (e.g. into the PDF Agent), or finish the isolation review first | NS-05, NS-16 | Before #3947 merges | Image change only | Medium | Amend #3947 | PDF / study-attention owner |
| 12 | Retention and erasure (after the E32 decision); ADR and MongoDB reference | NS-21, NS-17 | E32 at F1; implementation before production enablement | A TTL index applies to existing rows; the digest processor already tolerates missing items | Low | N12 | Notification programme |
4.1 Generic capture contract (C15 v2, PROPOSAL)¶
public interface INotificationKind { // registered by the owning feature
string Kind { get; } string EmailCategory { get; } NotificationScope Scope { get; } // Legacy/Canonical/Both
string AdmissionFlag { get; } int InlineRecipientLimit { get; }
Task<NotificationView> ResolveAsync(InboxNotification item, RecipientContext recipient, CancellationToken ct); }
public sealed record NotificationOccurrence(string Kind, Guid ProjectId, NotificationSourceRef Source,
string OccurrenceKey, DateTime OccurredAtUtc); // key from commandId/version/transition; never random
// SourceId = SHA-256(kind|sourceType|sourceId|occurrenceKey); Id = SHA-256(SourceId|recipientId)
public interface INotificationCapture {
Task CaptureAsync(NotificationOccurrence o, IReadOnlyCollection<Guid> recipients, ISessionHandle source, CancellationToken ct); // active txn, bulk $setOnInsert, <= limit
Task RecordFanOutAsync(NotificationOccurrence o, RecipientSelector selector, ISessionHandle source, CancellationToken ct); } // durable intent; leased expander
public sealed record NotificationView(NotificationAvailability Availability, string Title, string ActionLabel,
string? ActionPath, NotificationWorkflowState State, IReadOnlyList<string> ContextLines);
Capture happens only when three conditions hold: the kind's flag is on, the kind's scope matches the project mode, and the project is admitted for notifications. For recorded fan-out, the selector either holds a frozen recipient list (for example, the owners in a publication manifest) or names a capability that is evaluated with current authority when the intent is expanded.
4.2 Disclosure-policy hook¶
INotificationDisclosurePolicy.Apply(NotificationView, DisclosureContext{recipient, project, stages, channel = Inbox/Email/Digest, BL1 mode, VS1 policy, candidate or reconciler role}) runs after every resolver, for HTTP reads and both email processors.
Rules for every channel: - never show candidate answers, personal votes, other raisers' concerns, or a raiser's identity where blinding applies; - reviewer references are BL1 aliases from one stage-owned alias source, the same one candidate cards and threads use.
Additional rules for email and digest: title, project name (only if Q-N2 approves) and link only. No study titles, aliases or free text.
Every kind needs conformance fixtures covering each channel and role (candidate, reconciler, admin, revoked, blinded).
4.3 Merge order¶
- The ownership-transfer fix (#3964 or #3969, whichever Chris keeps). It edits
ProjectController.UpdateProject, which #3941 wraps. -
3932: regenerate the generated files on current
main; full checks on the head retargeted tomain; arun:e2e-fulllabel run; human approval.¶ -
3938.¶
-
3941: rebase on step 0 and test that a refused owner-reserved grant captures nothing.¶
-
3942, with change 1.¶
-
3943.¶
-
3944, then #3965 immediately after (restacked).¶
-
3945.¶
-
3947 last, after change 11.¶
X-NOTIF counts as met when steps 1–5 are merged with flags off. R1c also needs step 3; R4a's conversation work needs step 6.
5. Changes to the plan¶
5.1 Section edits by document¶
contracts.md- C15: replace with §4.1–4.2. Freeze gate becomes "F1 (contract); kinds per feature". Delete "as reviewAccessGranted already does" and cite StudyConversation and StudyIssue as the deterministic models.
- C3: add the exposure kind "questioned in reconciliation".
- C1, l.108: remove "#3944's candidate lookup" and add "conversations refuse canonical scopes until R4a".
- C9: RA5 exclusion; reconciler eligibility across bound stages; L6 owns conversations from R4a; study issues never carry answer disputes.
- C10: add the hook and the study-issue and PDF capabilities.
- C17: add the "My work" surface and where the inbox sits, and say UI1 applies to the stack's screens.
notifications-integration.md- §1.2: add #3965 (empty), the CI, E2E and review status, and the generated-file conflicts.
- §1.3: add the exact-set categories, the five writers, inbox reads gated by the capture flag, email that continues after the flag is off, environment-wide flags, and the behaviour that is live on merge.
- §2: RA5 exclusion as an R4a precondition; exposure fed into C3, R5c and R6.
- §3: add columns for occurrence key, capture mode, email category and scope; add the NS-15 rows; narrow Q-20.
- §4: replace with C15 v2.
- §4.1: add how the queues are surfaced.
- §5: remove the claim that #3950 tracks retention and the rest; add G-NOTIF, the X-NOTIF definition and the merge order.
integrated-plan.md- R1c: X-NOTIF entry criterion, or a conditional acceptance criterion.
- R2c: publication notices through recorded fan-out keyed by the publish operation; a completion notice to the publishing admin.
- R3c: a badge and banner that work with flags off.
- R4a: conversation refusal and rebind; RA5 exclusion; expiry through a scheduler marker.
- §5.11: define X-NOTIF as met; add G-NOTIF.
- §6.2: add a checklist item for notification enablement, with a rollback that includes the halt.
- §6.3: add the X-NOTIF edges.
- §7 W0: turn "notification event mapping" into the C15 v2 ADR.
- §8: merge protocol and ownership transfers.
- §10: add risks for category skew, email after disable, environment-wide flags, exposure leaking into agreement figures, and inbox floods.
domain-model.md- §2: list
NotificationEmailPreferences,NotificationEmailDelivery,NotificationEmailDigest,ImportNotificationAdmissionandCheckedPdfProposalwith its file record. - §4: InboxNotification gains an additive
Sourcefield (andResolvedAtUtcif Q-N3 says yes); StudyConversation gains the exposure record and canonical refusal; addNotificationFanOut. - §5: a capture mode for each operation (NS-23).
open-questions-and-assumptions.md- Narrow Q-20.
- Add Q-N1 to Q-N9.
- E30: add FEAT-024's outbox (#3107) and the two capture modes.
- Extend E32.
- Add E36 (C15 v2 ADR) and U30 ("My work").
- A-07: add per-project admission and halt.
migration-adoption-rollback.md- §1.4: add the #3945 and #3947 writers.
- §5: email continues after the flag is off, the halt, and how older binaries suppress unknown kinds.
source-status-inventory.md: refresh the states and add what is live on merge.decision-register.md§1.11 Q-10: record the RA5 status (Q-N9).
5.2 Event taxonomy to add to notifications-integration §3¶
| Email category (PROPOSAL) | Kinds | Capture mode | Occurrence key | Scope |
|---|---|---|---|---|
| Access and workload (existing two keys kept) | Review access; Reconcile access (activity field); workload (canonical version via C7) | Inline, capped | Mutation command + stage + activity | Both |
| Work assigned to me | Assignment created, expiry warning, expired or released; additional review requested, withdrawn or expired; concern needs applicability review | Inline; a scheduler marker for warnings and expiry | (assignment or request ID, transition, version) | Canonical |
| Results of my requests | Additional review returned; concern outcome (QY6); addressed by update (QY8); change approved or declined | Inline | (concern ID, outcome version), (request ID, decision) | Canonical |
| Approvals I need to give | LC1 change awaiting approval; outcome-migration manifest | Inline up to the cap, otherwise recorded fan-out | (change request ID, opened), (manifest ID, version) | Canonical |
| Changes affecting my reviews | Form version published (one kind); FV4 revision; profile version published; outdated annotations caused by someone else; task held; inputs changed | Recorded fan-out for publications; inline for one task | Publish operation ID, policy revision, (task ID, drift episode) | Canonical |
| Project changes | Adoption cutover; pilot admitted, removed or made read-only; publication applied or stalled (admin) | Recorded fan-out or inline | Wave, admission version, publish operation phase | Both |
| Existing kinds | Conversations (one-to-one); imports; exports; study issues; invitations (no email, NS-25) | Inline | As today | Conversations legacy until R4a |
5.3 Acceptance criteria and tests to add¶
New criteria for every release that adds a notification kind: - AC-C15-01: if capture fails, the source change rolls back; if the source rolls back, no inbox row remains. - AC-C15-02: replaying an occurrence, or resuming a half-applied fan-out, creates nothing new; two lifecycle transitions of one source create two items. - AC-C15-03: the registry test passes: every kind has a category, resolver, label and fixtures. - AC-C15-04: disclosure fixtures pass for each kind across inbox, email and digest, and across candidate, reconciler, admin, revoked and blinded recipients. A revoked recipient sees "Related item unavailable" and no email is sent. - AC-C15-05: when the kind's flag is off or the project isn't admitted, nothing is captured, and saved items stay readable. - AC-C15-06: an old client with a new server, and a new client with an old server, can both save an opt-out. - AC-C15-07: the halt pauses dispatch within one worker cycle and resumes without loss or duplicates. - AC-C15-08: fan-out above the cap goes through recorded fan-out; a 10,000-session publication gives one item per affected owner and stays within AC-R2c-06's fence target (B). - AC-C15-09: hints are coalesced: a burst of writes for one recipient causes at most one refetch per tab per second.
Changes to existing criteria: - AC-R1c-04: reword as conditional, and add a parity test between the decision paths. - AC-R2c-07: recipients equal the manifest's affected owners under the chosen policy, with no notice for doNothing; SourceId is the publish operation ID; the admin gets a completion or failure notice. - AC-R2d-08: narrow as in NS-14.
New release criteria: - AC-R3c-08: with flags off, a pending request shows in the approving admins' badge. With flags on, each admin gets one notice, and once one admin decides, the others see "decided". - AC-R4a-14: the assignment lifecycle produces exactly one notice per transition, and the expiry warning survives scheduler restarts without repeating. - AC-R4a-15: conversations refuse canonical scopes until rebound; reconciler eligibility holds across bound stages; RA5 reviewers stay excluded until they return their review. - AC-R4b-07: a two-raiser fixture shows each raiser only their own concern. - AC-R5c-05: a contribution saved after being questioned counts as informed. - AC-R6-07: manifests list conversations, issues and inbox items; after cutover, no saved link is broken. - AC-ALL-14: the rollback rehearsal includes the halt and an older binary running with new kinds present. - PE-06: no notice or email reaches a user outside the admitted pilot projects, checked against the inbox and Mailpit.
User-testing tasks: approve a pending change you were alerted to (R3c); find work assigned to you in a project you haven't opened (R4a); find out what happened to your query (R4b).
Stack tests before merge: the preference skew matrix; one-to-one disclosure tests for #3965; run:e2e-full on each retargeted head.
6. Questions for Chris¶
- Q-N1. First enablement scope. When notifications first go live in production, should they be limited to admitted pilot projects, or switched on platform-wide? And may staging and preview flags be overridden for testers? Recommendation: pilot projects first, and platform-wide only after the pilot exit review. Staging and preview overrides only with your approval for each release, with email kept in Mailpit.
- Q-N2. Email content. May notification emails and digests name the project? Study titles, aliases, answers and free text would never appear. Recommendation: yes, the project name only. Invitation emails already include it.
- Q-N3. Auto-resolve. When anyone resolves a workflow item (for example, one admin approves a stage change), should everyone else's related notices show "resolved" and drop out of the unread count, while staying in history? Recommendation: yes.
- Q-N4. Per-project email mute. Recommendation: yes, for email only; the inbox still records. Deliver it before production email.
- Q-N5. Material 3 timing for the stack's screens. Recommendation: the inbox and preferences screens before testers see them; the conversation, issue and PDF screens before production.
- Q-N6. Conversation records. Are reconciliation conversations part of the project's audit record, or private working notes? Recommendation: an audit record, kept while the project exists and exportable only behind an audit or export capability with aliases. Never included in candidate-answer exports or agreement figures, except as exposure markers.
- Q-N7. Q-20 refinement. Should "contains outdated annotations" notify only when someone other than the session owner caused it? Recommendation: yes.
- Q-N8. Delivery halt. Should production email require an operator pause that halts sending without cancelling it? Recommendation: yes.
- Q-N9. RA5 in Q-10. Your Q-10 decision as recorded doesn't mention excluding requested additional reviewers from conversations until they return their review, though the original recommendation did. Does the exclusion still apply? Recommendation: yes, as an R4a precondition, since RA5 doesn't exist before R4a.
7. Coverage gaps¶
- I didn't review #3965's implementation. It is empty on GitHub, and I didn't read its worktree because another agent is editing it.
- I ran no builds, tests, E2E or benchmarks. The thundering-herd and sequential-upsert costs come from reading the code and are UNVERIFIED by measurement.
- I didn't verify that
ISourceInboxCaptureis registered in the Project Management service; I inferred it from shared assembly scanning (UNVERIFIED). - I didn't check whether staging holds real user accounts (UNVERIFIED).
- I reviewed #3947's PDF security (scanner protocol, parser hardening) only as far as it affects merge order.
- I didn't read the provenance transcript or the Juniper thread. Whether Chris explicitly approved "accepted obligations survive disable" is UNVERIFIED.
- I didn't re-verify Review C's finding that the legacy reconcile route has no navigation links.
- Test counts are quoted from the stack's own documents.
- I didn't assess the digests' time-zone and daylight-saving logic.
- Angular components other than the inbox and preferences screens weren't checked against UI1.
Critical Files for Implementation¶
- /home/chris/workspace/syrf/pr/pr3617.research-screening-as-specialised-annotation-gxgahs/docs/planning/integrated-review-plan-2026-10/contracts.md
- /home/chris/workspace/syrf/pr/pr3617.research-screening-as-specialised-annotation-gxgahs/docs/planning/integrated-review-plan-2026-10/notifications-integration.md
- /home/chris/workspace/syrf/pr/pr3947.checked-pdf-proposals-and-explicit-administrator-a-r6bdl2/src/libs/project-management/SyRF.ProjectManagement.Core/Model/NotificationEmailPreferencesAggregate/NotificationEmailPreferences.cs
- /home/chris/workspace/syrf/pr/pr3947.checked-pdf-proposals-and-explicit-administrator-a-r6bdl2/src/services/api/SyRF.API.Endpoint/Services/Notifications/NotificationDetailResolver.cs
- /home/chris/workspace/syrf/pr/pr3947.checked-pdf-proposals-and-explicit-administrator-a-r6bdl2/src/libs/project-management/SyRF.ProjectManagement.Mongo.Data/Repositories/SourceInboxCapture.cs