Study Management Programme¶
Decision requested¶
Approve a phased redesign of the project Studies area, rather than merging any historical Study Management branch or treating PR #2990 as the finished feature.
The approved information architecture uses a Study management navigation group with three stable destinations. Short navigation labels are paired with precise page titles:
- Library / Study library — browse and filter study records; launch Bulk Study Update; show the current or latest update and link to its processing history (the history the server retains).
- Searches / Systematic searches — create and manage searches; show current or latest Search Import and Bulk PDF state in context (Risk of Bias omitted since 2026-09-02: the feature is disabled and on ice — see acceptance-criteria.md).
- Processing / Processing activity — the canonical read-only view of the retained history across permitted long-running operations, with links back to the owning screen.
This preserves the domain distinction between studies and searches while making long-running work consistently discoverable. It does not create a universal backend job type or place unsupported commands behind generic buttons.
MVP boundary¶
The first genuinely usable release is the smallest end-to-end slice that lets an authorised user:
- launch a Bulk Study Update from the Library and find the same operation in Processing after navigation or refresh;
- see exact status, truthful progress, bounded errors, initiator when the server can prove it, and valid created/updated/completed timestamps without year-one defaults;
- follow consistent links among Library, Searches, Processing, and the owning context;
- use the experience at 320 CSS pixels, at 200–400% zoom, with keyboard and screen reader; and
- receive stable state during loading, reconnecting, partial failure, completion, and live updates.
The shortest critical path is: contract truth → Claude Design iteration → shared presentation adapters → Library vertical slice → Searches consistency → Processing destination → final audit.
Cancellation and retry are capability-specific. Bulk PDF retains the commands it already supports. Search Import, Bulk Study Update, and Risk of Bias do not gain cancel/retry UI until a separate backend PR defines safe semantics and permissions for that job family.
Acceptance criteria for the plan gate¶
- Historical Study Management issues, PRs, branches, and feature documentation are reconciled.
- Current-main routes, APIs, projections, stores, job models, permissions, and deployed flags are inventoried.
- PR #2990 is classified as prototype evidence, with its correctness defects and useful parts recorded.
- Central and contextual job responsibilities are explicit.
- Material 3, responsive, high-zoom, accessibility, live-update, and state requirements are contractual rather than deferred polish.
- Implementation is split into independently reviewable PRs with file ownership, dependencies, focused tests, rollout, rollback, and staging proof.
- Product approves Library / Searches / Processing and the read-only Processing boundary.
- Claude Design completes the reviewed interaction and responsive design iteration (approved 2026-09-01; see design-decision.md).
- Security/product approve the permission matrix before any aggregate job view is exposed (Wave 1B supplies the server-filtered proof; gates Wave 5 exposure, not implementation start) (approved 2026-09-07).
- Material 3 PR #2994 lands or publishes its final token/navigation contract on main (merged; its contract is an ancestor of this plan's base).
A static (no rendered pass) design review of the shipped Library/Searches/Processing UI ran
2026-09-08/09 and found status-language, accessibility and page-shell consistency issues, grouped
into PR-sized fix slices. All slices have merged: A (Processing + dialog defects/a11y, PR #3413),
B (one status language, PR #3414), B2 (Searches table status language, PR #3420), C1 (shared page
shell + Library, PR #3412), C2a (Searches on the shared page shell, PR #3434), C2b (Processing on
the shared page shell, PR #3439), D (docs truth, PR #3411), and the docs closeout (PR #3435). These
are pre-C7.2 work: see acceptance-criteria.md section D for the tracker
entry and the acceptance rows it keeps Partial until the C7.2 rendered walkthrough.
Recovered history and disposition¶
| Artefact | Recovered intent | Decision |
|---|---|---|
| PR #1865 | Unified Study Management page with upload, Bulk Update, and progress | Closed prototype; retain user intent and test ideas, not code |
| PR #2511 | Monorepo rebase of the same combined page | Closed prototype; superseded |
| PR #2528 | Large admin/project job-progress implementation behind newStudyManagement |
Do not merge; preserve evidence, split by the waves in this plan, then close as superseded |
| PR #1875 | Tests for the pre-monorepo implementation | Preserve behavioural scenarios only; paths and component assumptions are obsolete |
| Issue #1917 / FEAT-018 | Original integration checklist and known last-modified mismatch | Replace the stale checklist with this programme; keep the issue as historical tracking until new slices exist |
| Issue #2562 | Production-ready refactor of the admin job progress UI | Separate admin-monitor successor; it does not own the project Studies experience |
| PR #2990 | Bulk Study Update history on Systematic Searches | Prototype evidence; do not merge in current form; supersede after the Library slice is reviewed |
| PR #2993 | Central Jobs hub plan | Reused as the canonical plan-only PR and broadened to this complete programme |
| PR #2612 | Staged Search Import design | Retain as a backend evolution dependency; do not absorb into the UI programme |
| PRs #2858, #2887, #2900 | Search-upload metadata and Bulk Update operational recovery | Retain and sequence; avoid their owned files |
| PRs #2976, #2979 | Bulk PDF runtime/release and hosting plans | Retain as authoritative Bulk PDF dependencies; avoid their owned files |
| PR #2994 | Material 3 tokens and adaptive-navigation contract | Read-only design dependency; Study Management retains product/domain ownership |
| PRs #2992, #3010 | About/navigation and Admin responsive work | Explicitly out of scope and untouched |
Why PR #2990 is not the end state¶
PR #2990 usefully demonstrates a bounded error disclosure, responsive table-to-card treatment, explicit Bulk Study Update status copy, and focused component tests. Those ideas should be reused after their data contract is corrected.
It diverges from the required programme in material ways:
- it puts project-wide Bulk Study Update history on Systematic Searches, although the launch action belongs in Library and canonical history belongs in Processing;
- it covers one job type without resolving entry points, navigation, refresh behaviour, or the relationship between contextual and central monitoring;
- it can report parsed rows as updated studies even when no study changed;
- its ordinary-member initiator lookup can display
Unknown Investigatorbecause the server projection omits secure related investigators; DateTimeLastModifiedis not mapped from the domainLastModifiedproperty, so the DTO can expose a year-one default and sort incorrectly;- it assumes no backend work is required despite those correctness and permission defects; and
- it does not reconcile Search Import, Risk of Bias, Bulk PDF, mobile/high-zoom behaviour, or the shared Material 3 status contract.
PR #2990 must remain untouched until this plan is reviewed. If the Library slice is approved, that implementation PR should explicitly supersede #2990 and carry forward only verified pieces.
Planning pack¶
- Current-state gap analysis and product design
- Phased technical implementation plan
- Target acceptance criteria tracker — updated by every wave PR
- Design handoff and proof matrix
- Approved design decision with the decision log, source truth and token mapping
Ownership and coordination¶
This plan owns Study Management product decisions and future files under a dedicated
project/study-management feature boundary plus the explicitly listed contextual screens.
Each implementation PR must publish its exact file ledger before editing and must not have a
second writer.
PR #2994 has merged, so this plan now consumes its Material 3 contract and may regenerate the
feature indexes and MkDocs navigation. PRs #2900 and #2979 still touch CLAUDE.md; the durable
agent-context pointer must therefore be added conflict-free after those owners land. This is a
sequencing dependency, not permission to modify another task's file.
Remaining product decisions¶
Use Claude Design to validate the Processing default, filter hierarchy, disclosure model, and contextual summary pattern before implementation.Closed — approved 2026-09-01 (design-decision.md).- Confirm project members may see only operation families already authorised by server projection.
Route visibility is decided by explicit
PermissionReportcapabilities — never by "absence of every family", which is indistinguishable from an authorised project with no jobs yet. Confirm history retention/pagination remains the current aggregate behaviour for the MVP.Closed — the brief scopes the MVP to the retained ProjectDetails aggregate with a paged historical API as evidence-triggered follow-up.
The Claude Design gate is accepted; decision 2's server-side proof lands with Wave 1B and gates Wave 5 exposure.
Current implementation summary¶
Relocated verbatim from the root CLAUDE.md.
- Canonical routes:
/projects/:projectId/studies(Library),/projects/:projectId/searches(Searches; the formeradmin/systematic-searchesURL redirects to it),/projects/:projectId/processing(Processing). - Processing is read-only history for three job families — Search Import, Bulk Study Update, Bulk PDF; Risk of Bias is deliberately excluded while that feature is parked.
- Route/navigation gating: Processing requires the generated default-off
studyManagementProcessingflag plus any of the six capabilities inPROCESSING_CAPABILITIES. Searches requiresviewSearches,importSearchorremoveSearch(hasSearchesCapability, mirroring the Search Import family's serverviewPermissions). With the flag off the Processing URL falls through to the in-project "This page isn't available" page (the project**child, nav still visible) and no navigation entry renders. With the flag off, the "Systematic searches" nav item uses the samehasSearchesCapabilityas its route. The Searches delete button renders only withremoveSearch; the stage Review row and route needstages[id].review; data export needsexportData(#4703). - Per-study PDF status (slices 8a and 8b; flagged
studyPdfStatus, default off everywhere, because it adds a collection, a background worker, outbound fetches and a new push): the server keeps onepmStudyPdfAvailabilitydocument per study (MongoDB reference), outsidepmStudy. The review and reconciliation retrievals return at once withPdfStatusand, when it is unconfirmed or due (Unavailable after 24 hours, Unverified after 15 minutes; Available and External are reused until the link changes), offer the study to an in-process queue (256, deduplicated per study). A worker runs 4 checks at a time, claims each study for two minutes across pods, and fetches only links built from SyRF's configured PDF bases, with the pdf-agent's check (PdfSignatureProbe: five bytes, 10 s, no redirects, no cookies). An external link is recorded as External without a fetch. The Library and single-study reads report the stored status and never start a check. With the flag off nothing is read or written andPdfStatusis null. Documents for deleted, moved or erased studies are ignored on read and go when the flag is retired. - Push (8b):
SubscribeToStudyPdfStatus(projectId, stageId, studyId)and the stage-lessSubscribeToProjectStudyPdfStatus(projectId, studyId)(bothProjectViewStudies, the presence lookups, a disabled member refused, refused while the feature is closed) sendStudyPdfStatusUpdated { studyId, status, checkedAt }, with no URL or file content, whenever a check changes the stored status. Every subscription starts with the study's current status, read only once the subscription is registered and decided by the same authority as every later update, so a check that finished before the panel subscribed (or while the client was disconnected; the client subscribes again on every new connection) still reaches it. v1 projects the availability change stream (the snapshot first,DistinctUntilChangedon status, ended on project revocation, current authority when enforced); v2 adds thepdf:{projectId}:{studyId}topic and aChangeFeedReaderforpmStudyPdfAvailability, flushed with the per-subscriber authority decision, and the hub asks the dispatcher for the snapshot once the registry entry is confirmed, so that read is ordered with every real change's. It isn't gated on active reviewer tracking. - Web (8b): with the flag on, the review PDF panel takes its state from the study's
pdfStatusand then the pushes, and never probes the PDF from the browser. Couldn't check offers Retry, which only reads the status again. A viewer load failure shows the viewer's own error and leaves the status alone. The Library PDF cell shows the stored state with an accessible name ("PDF: Not yet confirmed"). With the flag off, or when the server sends no status, both are as before, including the browser's PDF check. - Navigation with
studiesWorkspaceon (C+B′, W6): the project nav shows one "Studies" item instead of the Study management group. It's visible withviewStudiesand current on/studies,/studies/stage/:idand/searches, with exactly onearia-current="page"row. With the flag off, the nav is unchanged in bothstudyManagementProcessingstates. - Search read rules (SM-13, server-enforced):
GET api/projects/{projectId}/searchesneeds an active member holdingviewSearches,importSearchorremoveSearch(ProjectSearchReadPolicy); anyone else, including signed-in non-members of public projects, gets403. WithstudiesWorkspaceoff (or decided at its safe value, off), project details still give every member each search's id, name and study count for the Library, Overview and study filters, but without the description or original reference files unless the member holds one of those activities; with it on, W4's rule below applies instead. See classify an API endpoint's authorization. - Who can see search names (W4, server-enforced while
studiesWorkspaceis on): with the flag on, a member withoutviewSearches,importSearchorremoveSearchgets search names and study counts only while holdingViewSearchOverview, which has no default grant (a manager grants it to groups or to All members from the Searches page), and no searches at all otherwise. The same rule names the search in theSearchDeletednotification: other members are told a search was deleted without its name. The restriction applies only whilestudiesWorkspaceis on. With the flag off, or when the runtime decision falls back to its safe value (off), every member sees names and counts as on main (the search read rules above). Holders see no audit or living-search link either. With the flag on, the Searches page is the page for managing (W4): "Back to Studies", "Who can see search names" for managers who may change permissions (assignPermissions), and "Activity", linking to W3's/studies;activity=open. The project setup checklist's "Upload your systematic search" step also counts as done once the Library total is above 0, so adesign-only member who gets no searches still sees it done. - Studies workspace (C+B′, W1): with the generated default-off
studiesWorkspaceflag on,/studiesand its stage scope openStudiesWorkspaceComponent(studies/studies-workspace/) instead of the Library. It embeds the Library's own list (StudyTableComponentin workspace mode: compact Study, Search and PDF columns by default), a Searches pane (searchPaneAccess: the full pane for managers,hasSearchesCapability; the read-only "Searches (names and counts)" pane forviewSearchOverviewholders, from W4; none for anyone else) and a study detail frame. With the flag off, the Library, Searches and Processing are unchanged. Scope and seams: the implementation plan's "W1 · Workspace shell". PDF filter and pane PDF counts (#4719, PR #4764): withstudyPdfStatuson as well, the filter bar adds a single-choice PDF filter (Any PDF status, Without a confirmed PDF, Available, Not yet confirmed, Unavailable, No PDF;;pdfStatus=in the URL) and managers' settled pane rows add "N without a confirmed PDF" (GET …/studies/pdf-status/search-counts), both from slice 8's server-tracked state; the server refuses the filter (409) while either is off. It is ANDed with a My studies tab (#4720): a request may carry both, and the list and its count are their intersection; a malformed tab (400) or unknown stage (404) is refused before the PDF filter's 409. The Searches page and Overview reuse the counts for search managers (#4770): each settled Searches page row with a count above 0 links "N without a confirmed PDF" to Studies on that search and Without a confirmed PDF, and Overview's Studies line adds " · N without a confirmed PDF" for the project, from one shared loader (shared/search-pdf-counts/) fetched once per page view and refetched on the pane's events. See the implementation plan's W1 as built, decision 1, and "Searches page and Overview counts (#4770)". - Bulk study update in the workspace, server half (BSU-a, #4766): three read-only fields, each null while
studiesWorkspaceis off. Each bulk study update job carriesallOrNothing(its own stamped execution version is 2, ADR-020), in project details, the project stream and the upload-failure report, only to members who already see the job. Project details carrynewBulkStudyUpdatesAllOrNothingfor members holdingbulkUpdateStudies: whether a job started now will run all-or-nothing, read from the API's own deployedbulkStudyUpdateAtomicApply, the value that stamps new jobs, so the two can't disagree (there is no web flag). The Library list carriesbulkUpdateLockedAton each study an all-or-nothing update holds: the stored lock that study writes are refused on, so it says nothing the 409 doesn't. Nothing new is stored, and no UI reads them yet (BSU-b). - Bulk study update per-row unmatched detail (BSU-c, #4810; behind
studiesWorkspace, no new flag): a bulk study update job created while the flag is on is stampedRecordsUnmatchedRows(once, at creation). A stamped version 1 job records up to 100 rows naming no study in the project, with their study IDs (parseResult.unmatchedRowSample); a stamped version 2 job'sStudyNotFoundproblem carries the study ID. Counts are unchanged, an unstamped job stores nothing new, and the API sends the detail only while the flag is on. Activity then lists "Row: no study with ID in this project." instead of the count line. As built: implementation plan, BSU-c as built. - My studies tabs, server half (#4720, PR A; behind
studiesWorkspace, no new flag; the web half is listed next): the server decides the reader's own work in a stage. The reader is always the signed-in user (under support impersonation, the impersonated member): no query, route or body field names a reviewer, and no response carries an investigator id or another reviewer's decision. MyStageWorkper study (Core/Services/MyStudies):InProgress,Completed,OpenToStart,NotStartedNotOpen,FinishedorNotInStage. A stage is screening-kind when it reviews screening, or screening and annotation with studies selected by screening (StageWorkKind, the twin of the web'syourStatusFilterFor); every other stage is annotation-kind. Annotation reads the reader's own non-reconciliation sessions in the stage (an unfinished one wins over a completed one), then exclusion, then the stage's allocation target (SufficientlyAllocated, counting reservations only while tracking is available), then, withproportionalStudyAllocationon, the reader's share (an invalid configuration opens nothing). Screening reads the reader's own screening, then whether the study is sufficiently screened.MyStageWorkFilters(Mongo) andMyStageWorkEvaluator(in memory) agree for every study and reuse the review page's reviewer-stats predicates.- Team-excluded studies follow the review page's My studies panel (settled with the owner, 4 November): with the stage's hide-excluded setting on, an excluded study is in no tab and no count, completed or in progress, because the panel hides every excluded saved study. With it off, excluded work stays the reader's own (In progress or Completed), as the panel's rows list it and its summary counts it when excluded sessions are grouped with the rest. This is deliberate, not an asymmetry to "fix": it is not the progress meter's raw sum.
- Tabs: To do =
InProgress+OpenToStart; In progress =InProgress(always 0 in a screening-kind stage); Completed =Completed. - List:
POST api/projects/{projectId}/studies(unchanged policy,ProjectViewStudiesPolicy) readsfilters.myStudiesTab(None,ToDo,InProgress,Completed) while the flag is on, ANDed with every other filter. A tab needsfilters.stageId(400my-studies-tab-refusedwithout one, or for an unknown tab; 404my-studies-stage-not-foundfor a stage not in the project; both are ProblemDetails). Each row carriesmyStageWorkwhenever the request names one of the project's stages, tab or not, and the response saysmyStudiesTabApplied: trueonly when the tab was applied. Off, the tab is ignored,myStageWorkis null andmyStudiesTabAppliedfalse. - Counts:
GET api/projects/{projectId}/stages/{stageId}/my-studies/counts(StageReviewPolicy, FeatureGated: unroutable 404 while off) answers{ inProgress, notStarted, completed, toDo }, stage-wide (no list filter), 404 for a missing project or stage. ThreeCountDocumentsAsynccalls over the project; a count no study can meet (In progress in a screening-kind stage) is 0 without a query. The list's tab and the counts build their scope through oneMyStudiesScopeResolver(oneproportionalStudyAllocationread throughIFeatureDecisions), so the To do total and the To do count can't drift. - Indexes: none new. Explained on synthetic data with the study collection's indexes
(
MyStudiesCountsExplainTests): every count is anIXSCAN; In progress and Completed fetch only what they count; not-started counts scan the project's index range, as the review page's available counts do. - Nothing is stored in any flag state, so switching the flag back and forth leaves no data behind.
- My studies tabs, web half (#4720, PR B #4775;
studiesWorkspaceonly, no new flag): in a workspace stage scope a reader who may review the stage (stages[id].review) gets To do, In progress and Completed with the counts, the lede "Your studies in: the ones you’ve started or been given." and the line " in progress · not started · completed" while no other filter narrows the list. Managers see the whole stage and "Only mine" (in the filter bar) when they may review it. While the tabs show, "Your status" is hidden and cleared: the guard rewrites it to All in the URL, so the form holds All and Only mine off doesn't restore it. Every reader in a workspace stage scope (tabs or not: managers with Only mine off and readers who may not review included) gets the "Your " column, the reader's own MyStageWorkper row: Start or Resume (active stage,stages[id].review), In progress (started, no Resume here), Not started, Completed, Finished, or "—" forNotInStage(excluded at screening with no own session; with the stage's hide-excluded setting on, every excluded study). The tab is;myStudies=todo|inProgress|completed(core/state/ui/my-studies-tabs.ts), fetched by the workspace'sstudiesWorkspaceTableGuard; the review page's "See them all in Studies" opens;myStudies=todo. A tab answered withmyStudiesTabAppliednot true shows "Your studies couldn't be loaded." with Try again and no rows. With the flag off the Library ignores and drops;myStudies. Details: the implementation plan's #4720 "Web as built". - Stage chooser chip (#4776, PR #4808;
studiesWorkspaceonly, no new flag, web only): the workspace's filter bar has "My studies" (members) or "Stage" (managers), a menu of "All studies"/"All stages" and every stage the reader may review (stages[id].review, active or not). A member's choice opens the stage on To do (;myStudies=todo); a manager's opens the whole stage, or To do while Only mine is on. The search filter, title search, PDF filter and page size carry over. Members outside a stage scope see "Choose My studies to see your work in a stage." Details: the implementation plan's #4720 "Stage chooser chip (#4776)". - PDFs to check (slice 9, PR #4785; behind
studiesWorkspace, no new flag, plus an index admission): R2's Set holders (ApprovePdfCorrections) see "N proposed PDFs to check" first on Overview's Needs attention card, with Check proposals, which opens the workspace filtered to "Proposed PDF waiting" (appended to #4719's PDF filter; it needs nostudyPdfStatus). The PDF cell reads "No PDF · N proposed" or "Unavailable · N proposed" withstudyPdfStatuson, and the detail names the proposer for Set holders only. Server:GET …/studies/pdf-proposals/summaryandGET …/studies/pdf-proposals?studyId=…(Set only, FeatureGated,Available: falsewhile the0035pmStudyPdfCorrectionindex is missing, throughStudyPdfProposalsAdmission), and the filter value (403 without Set, 409 off, closed or above 10,000 studies). Use proposed PDF writes the link and the decision in one transaction and Reject is one conditional write; both are idempotent, and a race leaves the study and its proposal agreeing. Off, Approve and Reject are as before. See the implementation plan's slice 9 section. - Shared status language:
app-study-job-status-chip+presentStudyJobStatus, deployed labels verbatim, "Unknown status (n)" for unknown enums, no invented progress. - Overview's study count is the Library total (
GET api/projects/{projectId}/studies/count,ProjectViewStudiesPolicy, fromCountStudiesInProjectAsync), fetched afresh on every Overview visit (the state is provided byProjectOverviewComponent, so nothing is fetched while you are on another page) and linked to the Library; it is not the sum of the searches' imported counts. While it loads nothing shows. When it is 0, Overview says "No studies yet." if the project has searches, otherwise "Add a search to begin." to someone with the project'sdesignpermission and "No studies uploaded yet." to everyone else. - Authority: this directory (README, implementation-plan, design-decision, design-decision-log, design-source-truth, acceptance-criteria); flag enablement is gated on B17 sign-off and the C7.2 staging walkthrough recorded there.
Glossary¶
The study-management terms used in the app and the user guide. Keep the app copy and the user guide glossary in step.
- Library: every study currently in the project, wherever it came from. Its total is the number of studies in the project.
- Search: one set of records you imported from a search strategy or database. Each is a systematic search; the app calls it a search. A project can have many.
- Import a search: the action that brings a search's records into the project. It starts from the Searches page, whether or not the import wizard is switched on.
- Records and studies: records are the rows in the file you import, counted per search on the Searches page. Studies are what SyRF holds after the import. The two numbers can differ, for example when rows cannot be read.
- Import notes: anything SyRF tells you about a file while importing it, shown against the search.
- Uploaded: a PDF file has reached SyRF. It does not yet mean the PDF belongs to a study.
- Attached: a PDF is linked to its study, including one that was already stored.
- Verified available: SyRF fetched the stored file the way a reviewer's viewer will. Otherwise a PDF is Unavailable or Not checked.
- Not matched: files left on your computer because no study matched them.
- Already stored: files SyRF already held, so they were not sent again.
- Studies without a PDF: studies that have no PDF attached.
- PDF states: where per-study PDF status is switched on, a study's PDF is shown as No PDF, Not yet confirmed, Checking, Available, Unavailable, Couldn't check or Hosted elsewhere. SyRF checks a study's PDF when the study is opened for review, and the review page updates when the check finishes.
- Without a confirmed PDF: every study whose PDF isn't Available, including studies with no PDF. The Studies workspace's PDF filter offers it, and for search managers the Searches pane and the Searches page count it for each search, and Overview counts it for the project.
- Activity: the read-only history of imports, bulk study updates and bulk PDF uploads, with where each got to and what went wrong. Where it is switched on it appears as Processing in the project menu.