Skip to content

Selected reviewer reconciliation questions

The minimal usable slice lets a freshly authorized reconciler question candidate reviewers about the current project, study and stage. Every question opens a private one-to-one conversation between the reconciler and exactly one candidate reviewer: selecting several candidates opens one thread per candidate, so no reviewer can read another reviewer's question or reply. A durable conversation retains its two participants, the questions and the replies; each committed post captures a notification for the other participant only, in the same transaction. Saved notification links reopen the private thread after refresh or sign-in, and a notice can be answered from its card without opening the thread (the quick reply, below).

History. The first slice (#3944, on main as notifications restart L6, 28 October 2026) showed every post to every selected reviewer (gap C7). The privacy fix (#3965) was written on the notification stack and re-applied on main by cherry-pick as restart L7 (#4673, owner decision Q1, option B); the L6 and L7 sections of the technical plan hold the numbered acceptance criteria.

Acceptance: authoritative candidate selection, current stage grants and account admission, participant-only reads/replies, blinding-safe labels, retry deduplication, atomic capture and no changes to canonical annotation/reconciliation answers. Routine study claims stay silent. The shortest critical path is domain context/participant admission, persistence and inbox shaping, question/reply controls and security/transaction/browser verification.

One thread per candidate

  • Identity. A thread's ID is a SHA-256 over the reconciler, project, study, stage, the candidate's session and the client request ID, so one request fans out to distinct threads. Post IDs derive from the thread, so every post, and therefore every notification source (capture is an insert-only upsert unique on recipient, kind and source), is distinct. A reused source ID would silently drop a later notification, which is why no two threads share one.
  • Recipients. A reconciler's post notifies that thread's candidate only; a candidate's reply notifies the reconciler only. A candidate reads only their own thread and cannot post into another candidate's.
  • Atomic fan-out. All threads of one request (at most 20) and their notices commit in one majority transaction, or every requested thread already exists unchanged and the request replays, or none is written. A retry that changes the text for any candidate, or names a candidate the saved request did not include, is refused whole as a typed conflict; it is never topped up. A retry naming only already-saved candidates replays. The web form starts a new request identity after any answer from the server (a conflict, or "no longer eligible") and on every reload, and keeps the identity only for a send the server never answered.
  • Who can be questioned. Only reviewers whose own non-reconciliation session for that study and stage is Completed, and who currently hold stage Review authority. Incomplete sessions and reconciliation sessions are never candidates. The server rechecks this on every request, and a reconciler's follow-up needs the candidate session still completed (the candidate may always answer while their session exists).
  • Stable aliases. Reviewers are named by a study-local alias, "Reviewer 7Q" (owner decision, 2 November 2026: never "Candidate 7Q", and no longer L6's running "Reviewer N"): a digit and a letter drawn from a hash of the study and the session, so an alias is fresh for each study and says nothing about order. Draws are taken in the creation order of all the stage's non-reconciliation sessions for the study, whether or not they are offered, and a collision moves the later session to its next draw, so one reviewer reopening or finishing a session never renames another and a session added later never renames an earlier one. The label on a candidate card, a saved thread, its summary and its notices is resolved from the candidate's session at read time, so they always agree; the label stored when the thread opened is only the fallback for a session that has since been removed.
  • About one form question. A question may optionally name one question of the stage's form (owner decision Q3); the server refuses any other ID and stores it on every thread of the question. Both participants see the question's current text; one removed from the form reads "A question no longer in the form".
  • Who can ask. A Reconcile holder who has reviewed the study themselves, meaning any of their own non-reconciliation annotation sessions on it, in any stage and in any status (a draft counts), cannot start a conversation. The rule is deliberately conservative and does not depend on the stage being reconciled, whereas who can be questioned stays per stage. The candidates and create endpoints answer 409 with the typed problem code ReconcilerReviewedStudy, and the same rule blocks their follow-ups with the same typed refusal. Saved history stays readable. Only a review counts: the reconciler's own reconciliation sessions do not, and neither does a screening decision, which is not an annotation session. The refusal is about that reconciler alone: the candidate may still answer, and another Reconcile holder may still ask.

Exposure record and indication

The conversation records, on itself and never on the Study, which candidate session was questioned, its status when first questioned, and when the first question and the candidate's first reply happened. Each of the candidate's own posts also records the status their session had when they posted, so a reply given after the session was reopened can be told apart later. This record is for the asking reconciler: their conversation page shows it under Details ("Questioned during reconciliation", the review's status when first asked and the first question and reply times), and the candidate does not see it. The reconciler also sees a read receipt ("Read 10:22") under each of their messages the candidate has opened (owner decision Q13): the first time the candidate opened its notice or the conversation, or replied after it, written once and never changed by marking notices read or unread. The candidate is told "The reconciler can see when you read this." and sees no receipts.

Every Reconcile holder for the study and stage, not only the asking reconciler, sees a content-free questioned marker on the candidate: when the candidate was first questioned (by anyone) and whether any reply exists. It never carries a thread, its text, who asked, or any other candidate. It is answered by a repository query that projects only the exposure fields, served by L6's conversation index (0033: project, study and stage first), and it shows as a Questioned during reconciliation badge beside the candidate in the question form and on the candidate's card on the reconciliation page. Only the asking reconciler sees the thread. Candidate-side visibility is unchanged: a candidate sees their own thread and nothing about who else was questioned.

No read-only study view exists for reviewers, so a candidate's context link is the thread itself (/attention/{id}), which is also the notification deep link, and the project, stage and study title are shown as text. The reconciler's link goes to their own reconciliation page. Neither is an editable review session.

Quick reply from the notification card

A conversation notice the reader can still reply to (the same rule as the conversation page's reply form) carries a Reply action. The card is N4's shared notice card in the layout the owner chose on 2 November 2026 (option 1 of the question notice board): the headline says who wrote ("Question from the reconciler", "Reviewer 7Q replied"), the context line names the project, then "Stage · Study", each message is a speech bubble with its author and time (a reply also shows the reader's own last message, clamped to three lines with Show all), and Reply, the last and only filled button, opens the message composer in place: a rounded box with Send inside, the privacy line naming the one reader ("Only the reconciler will see this. It does not change any answers."), and Cancel. Send reads the thread's current version and posts through the conversation's own reply route, so the server checks authority again and notifies only the other participant. A refusal is shown in plain words, and Open conversation stays on the card. Nothing is offered when the reader can no longer reply (the stage is inactive, the candidate's session is gone, the reconciler reviewed the study, or the thread holds 100 posts). The page and the bell panel reply the same way.

Replying answers the notice (owner decision Q5): the post resolves, in its own transaction, every notice its author received in the thread. The card shows the sent reply as the reader's own bubble and "You replied" at once, and stays where it is until the reader leaves Notifications or closes the panel; then the notice lives under Resolved. A new message from the other person is a new notice.

Flag decision

reconciliationConversations (feature reconciliation-conversations, Release, default off, runtime-manageable, API host with a reviewed consumer contract) declares studyAttention as its dependency, and studyAttention keeps its dependency on notificationInbox. Conversation endpoints, the reconciliation page's question form and history, the /attention/:id route and the conversation notices all need the three on, decided from one capture. While any is off the six endpoints answer 404, saved threads stay hidden, and conversation notices read as "no longer available". studyAttention alone never opens conversations; it stays the study attention family switch (study issues and checked PDFs follow in L8 and L9).

Until L7, studyAttention was deployment-managed (pinned) because of gap C7. L7 lifts that pin: a non-production environment may switch studyAttention at runtime again, and switching reconciliationConversations on while studyAttention is off is refused with the typed unsatisfied-dependency answer unless the administrator confirms the dependency.

Rollout order and legacy documents

Data is additive. The thread shape changed (Reviewer replaces Reviewers, plus the exposure record), but no document is converted. A conversation saved by the L6 build, which was only possible where studyAttention was deployed on (the E2E stack and local stacks; it was pinned off everywhere else), has a Reviewers list and no single candidate, so it cannot be read as a one-to-one thread: reads skip it instead of failing, GET answers 404, a reply conflicts, and its notices read as unavailable. The API logs one warning per process carrying only how many it skipped, never their content. Nothing deletes it.

Rolling back to the L6 build is safe: that build pins studyAttention to its deployed value, which is off in every environment, so it never reads a one-to-one thread.

Rollback, and what hiding conversations costs

Setting the flag off is the kill switch and takes effect at the next request. It is a deliberate hide: conversations disappear while their flag is off. Weigh these before turning it off where people have live conversations:

  • Saved conversations vanish for both parties. The endpoints answer 404, the /attention/:id page is not found, the reconciliation page shows no question form, history or questioned badge, and their notices read as "no longer available". Nothing is deleted; it all returns unchanged when the flag is turned back on.
  • Hidden notices leave the unread count and stay in history (N2's rule: the count is unread, unresolved, not dismissed and available at read time), so the bell never counts a notice the recipient cannot open.
  • Emails pending while the flag is off are dropped, not deferred. A delivery whose notice resolves as unavailable is suppressed for good (NotificationEmailProcessor, NotificationDigestProcessor), so conversation emails that were waiting are not sent when the flag comes back. Nobody can opt in to these categories from the web yet.
  • A reconciler loses the thread, and a reviewer cannot answer, for as long as it stays off.

Behaviour retained

Existing review and reconciliation paths remain intact. Accepted conversations retain history through later role/candidate changes while the flags are on, and every visible detail and reply action rechecks current authority. Email uses the existing category preferences and shared immediate/daily ledger; no separate transport.

Study issue reports and quarantined PDF correction are the remaining ordered slices (L8, L9).

Authority, bounds and presentation

The server binds candidates to saved non-reconciliation sessions in the same study and stage that are completed. It checks fresh active accounts, active project membership and current stage Review/Reconcile grants without application-admin shortcuts. Selected session IDs are admitted by the server; clients cannot supply participant identities. Stable request/post IDs deduplicate retries; changed payloads and stale revisions refuse writes. A stage in the canonical review model is refused like a missing one (L6).

History retains the two participants. Later removal of the candidate's session disables that reviewer's replies; current role withdrawal hides protected history and turns related notices unavailable. Inactive stages retain authorized history and disable replies. DTOs contain generic labels ("Reconciler", "Reviewer 7Q"), never names, emails or investigator IDs. Free text is rendered as text.

Initial bounds are 20 selected sessions, 4,000 characters per post, 100 posts per conversation and 100 recent conversations per study/stage/participant query. A full conversation becomes read-only; new questions remain independent. Paging and closed-thread controls are ordered follow-ups.

Validation

Original slice (#3944): 383 focused API checks, four physical Mongo replica-set transaction checks and 29 Angular tests passed, with changed-file lint, generated-code validation and Release builds. The full browser journey passed: two reviewers save conflicting answers, only the selected reviewer receives the question, a foreign participant is refused, replies reopen after reload and both parties follow their saved notification links. Canonical ExtractionInfo is byte-for-byte unchanged by the conversation. The existing E2E_BROWSER_NETNS=1 harness isolates Chromium from sibling Docker interface changes on this shared host.

Privacy follow-up (#3965) on its own branch: six aggregate tests, 15 physical Mongo replica-set tests and 35 conversation tests in the API unit project passed, with 286 Angular tests in 22 files. The counts for L7 on main (the same tests on main's architecture, plus the quick reply and the lifted pin) are in the PR.

Saved authorized conversation

The screenshot is the candidate's view of the saved conversation: the private-thread notice, the project, stage and study as plain text with no study link, the two participants, and the reply form. The questioned indication is only shown to Reconcile holders (beside the candidate on the reconciliation page, and on the asking reconciler's conversation page), never to the candidate, so it does not appear here.