Skip to content

Publishing & Versioning UX

Addendum to the Question Management UI Specification. Covers the entity model, publishing workflow, draft/published state representation, validation, and publish button placement across Design, Assign, and Preview views.


Table of Contents

  1. Entity Model
  2. State Lifecycle
  3. Publishing Workflow
  4. Design View -- Draft & Published States
  5. Assign View -- Two Visual Dimensions
  6. Preview View -- Draft vs Published
  7. Publish Button Placement
  8. Pre-Publish Validation
  9. Cross-Question Validation in Design View
  10. PQS as Implicit vs Explicit Concept
  11. Resolved Decisions
  12. Remaining Open Questions
  13. Prototype Design Decisions Addendum
  14. Autosave Failure and Offline Resilience
  15. Annotator Re-Answer Experience

1. Entity Model

Overview

The domain model separates draft questions (design-time, fully mutable, embedded in the Project aggregate) from published annotation questions (versioned, structurally frozen, in their own aggregate/collection). The publish ceremony promotes drafts to published entities.

See implementation-plan.md Phase 1 for the complete data model with entity/value object classifications, mutability rules, and MongoDB collection layout.

Key Entities

Type Classification Where Lifecycle
DraftQuestion Entity (within Project aggregate) pmProject.DraftQuestions[] Fully mutable. Deletable. Created in Design view. Destroyed on publish (promoted to AQ).
AnnotationQuestion Entity (own aggregate) pmAnnotationQuestion collection Created on first publish. Structural props frozen. Cannot be deleted. Content changes via draft field.
ProjectQuestionSet Entity (singleton on Project) pmProject.ProjectQuestionSet One per project. Contains mutable draft + append-only immutable versions. Internal only -- never referenced in UI.
StageQuestionSet Entity (singleton on Stage) Stage.StageQuestionSet One per stage. Contains mutable draft + append-only immutable versions. Internal only -- never referenced in UI.

UI terminology rule: The terms PQS, SQS, "Project Question Set", "Stage Question Set", and "question set version" must NEVER appear in user-facing UI text. Admins think in terms of "the current questions" vs "your changes", "publishing to a stage", and temporal labels like "Update 3 -- Mar 28, 14:32" when identification is needed. These internal entity names are valid in domain model documentation and code, but not in any UI copy, tooltips, labels, or dialogs.

Key Value Objects

Type Immutable? Purpose
AQVersion Yes (has reference ID for annotations) Immutable content snapshot. Created on publish.
DraftContent No (mutable, autosaved) Pending content changes on a published AQ. Null when clean.
PQSVersion Yes (has reference ID) Immutable snapshot of ordered question references.
SQSVersion Yes (has reference ID) Immutable snapshot of stage assignment (unordered set + PQS version ref).
QuestionReference Yes Typed reference in draft PQS: {questionId, type: "draft" or "published"}
QuestionRef Yes Reference in published PQS: {questionId, versionId}
DraftPQS No (mutable) Current ordered arrangement of all project questions.
DraftSQS No (mutable) Current unordered set of questions assigned to a stage.

Ordering

  • Root question ordering: Encoded in the draft PQS / PQS version's ordered array.
  • Child question ordering: Derived by filtering the PQS array to children of a given parent (using parentQuestionId on each question). The subset's order in the PQS array defines child ordering.
  • Stage question ordering: Derived from the PQS ordering filtered by the SQS question set.
  • No ordering stored on individual questions or on the SQS.

Autosave

"Autosave" means the client debounces changes and sends them to the server automatically. The server persists:

  • Draft question changes: Written to DraftQuestion entities on the Project (for unpublished questions)
  • Published question content changes: Written to the draft field on the AnnotationQuestion document in pmAnnotationQuestion
  • Assignment changes: Written to StageQuestionSet.draft on the Stage
  • Ordering changes: Written to ProjectQuestionSet.draft on the Project

This is the PR2398 pendingChanges mechanism with an automatic trigger. All draft data is server-side — nothing is stored only in the browser.


2. State Lifecycle

Question States

Each annotation question can be in one of these states:

State Description Visual Indicator
Draft (new) Never published. Only exists as a draft. "Draft" label
Published (clean) Published and no pending changes. No indicator (clean = default state)
Published (with unpublished changes) Published, but has unpublished edits on top. "Published with changes" indicator

Note: Version numbers are NOT shown on question nodes. Only "Draft" (never published) or "Published with changes" (published, with unpublished changes) indicators appear. Version history with dates/timestamps is available in the properties panel and version history dialog.

Autosave Model

There is no manual "Save" action. All draft changes are autosaved (debounced, every few seconds of inactivity). The only deliberate action is Publish. This follows the Figma/Google Docs model.

Friction Model

The system uses graduated friction that matches the stakes of each action:

Phase Friction Level User Experience
Editing drafts (Design view) Zero -- autosave, no confirmation Feels like Figma/Google Docs
Assigning questions (Assign view) Low -- checkbox toggles, autosaved Lightweight configuration
Publishing (Assign view ceremony) Intentional -- review dialog, change reasons, confirmation Matches the stakes: creating immutable versions

Publishing only happens when assigning questions to a stage, which is the moment the admin is committing to 'this is what annotators will see.' That is the right place for ceremony.

Draft safety net — two layers:

Layer 1: Undo/redo stack (session-level, client-side)

  • Operates at the whole question management session level, not per-question
  • Every autosaved draft change is an operation in a single undo stack (edits, additions, deletions, reorders, assignment changes)
  • Cannot undo past a publish boundary -- published versions are immutable. The undo stack resets after a stage publish.
  • Lost when the browser session ends
  • Autosave interaction: Undo/redo cancels any pending autosave debounce and restarts the timer with the new state. This prevents stale intermediate states from reaching the server during rapid undo/redo sequences. Only the settled state (after the user stops pressing undo/redo for the debounce period) is autosaved. The server always receives consistent state.

Layer 2: Draft snapshots (server-side, persistent, self-service recovery)

Periodic snapshots of the entire draft state (draft PQS ordering + all draft question contents), stored on the Project. Provides Google Docs-style version history for drafts.

What gets snapshotted: Only draft state -- DraftQuestion contents and published AQ draft fields. Published AQ versions are already permanent and don't need snapshotting.

Deduplication: If a question hasn't changed since the last snapshot, the entry references the previous snapshot rather than storing a copy. Only changed questions store full content.

GFS (Grandfather-Father-Son) retention:

  • Son: Every 15 minutes of active editing -- kept for 24 hours
  • Father: One per day -- kept for 7 days
  • Grandfather: One per week -- kept for 30 days
  • Hard cap on total snapshots per project (e.g., 50)

Snapshot triggers: Every N minutes of active editing, on navigation away from Design view, on session end, before publish (captures pre-publish draft state).

Browsing and restoring: Accessible from the Design view via a "Draft history" action. Shows a timeline of snapshots with timestamps. Admin can:

  • Restore entire draft: Replaces the full draft PQS ordering and all draft question contents with the snapshot state. The current state is snapshotted first (so the restore itself is undoable).
  • Restore a single question: Replaces just one question's draft content from the snapshot, leaving everything else as-is. Safe because content and ordering are separate concerns.

Draft history UI (in the Design view, alongside version history):

DRAFT HISTORY (recent autosaves)
  Today 14:32   3 questions changed                [Restore all] [View details]
  Today 11:15   1 question changed                 [Restore all] [View details]
  Yesterday 16:45   5 questions changed             [Restore all] [View details]
  Mar 20 (weekly)   12 questions changed            [Restore all] [View details]

  [View details] expands to show individual questions:
    - "What kind of TBI model?" (text changed)      [Restore this question]
    - "Drug Name" (options changed)                  [Restore this question]
    - "Species" (unchanged)

Annotation Count

Questions that have been answered by annotators show an annotation count indicator on the node. The properties panel shows: "12 studies, 47 annotations. Content changes will create a new version on publish."

Stage Question Set States

State Description
No SQS Stage has never had questions published. Only draft assignments exist.
Published (clean) Published SQS, no pending assignment changes, all AQ versions current.
Published (draft assignment changes) Published SQS exists, but admin has added/removed questions in draft.
Published (outdated AQ versions) Published SQS exists, but some AQ versions are older than the latest PQS.
Published (draft changes + outdated) Both pending assignment changes and outdated versions.

3. Publishing Workflow

Trigger: Per-Stage Publishing with Cascading Validation

The admin publishes a stage, not a PQS directly. The PQS is updated implicitly as a side effect.

Admin clicks "Publish [Stage Name]"
  |
  +--> 1. VALIDATE: Check all questions in the draft SQS
  |      - Cross-question coherence (conditional references, parent integrity)
  |      - All required fields filled
  |      - No broken option references
  |
  +--> 2. If INVALID: Show errors, block publish
  |      - List which questions have problems
  |      - Link to each problem in Design view
  |      - Publish button remains disabled
  |
  +--> 3. If VALID: Create immutable versions
         |
         +--> 3a. For each AQ in this SQS that has draft changes:
         |      - Create new AQ version (v2, v3, etc.)
         |      - Clear draft state on that AQ
         |
         +--> 3b. Create new PQS version
         |      - References latest AQ versions for questions in this SQS
         |      - References existing AQ versions for all other questions
         |
         +--> 3c. Create new SQS version
         |      - References the new PQS version
         |      - Contains the ordered subset of question IDs
         |
         +--> 3d. Stage goes live / updates live form
                - Annotators see the new SQS version

What Happens to Questions NOT in This Stage's SQS

  • Questions assigned to other stages but not this one: Unchanged. Their draft changes (if any) remain as drafts.
  • Questions not assigned to any stage: Unchanged. They stay as drafts.
  • The new PQS version includes the previous versions of these unchanged questions (carried forward from the last PQS version).

What Happens to Other Stages After Publishing

  • Other stages' SQS versions still reference their existing PQS version. They are not auto-updated.
  • If a shared question got a new AQ version (because it was published with this stage), other stages still see the old version.
  • The Assign view shows a notification on those stages: "N questions have newer versions available."

Publish All Stages

An optional "Publish all stages" action that:

  1. Validates ALL draft SQSes across all stages simultaneously
  2. Creates new AQ versions for all questions with draft changes
  3. Creates a single PQS version
  4. Creates new SQS versions for all stages
  5. Useful when a question change needs to be coordinated across stages

4. Design View -- Draft & Published States

Node Visual Indicators

Each question node in the tree shows its current state:

State Visual Treatment
Draft (new, never published) "Draft" label in muted style.
Published, clean No indicator (clean is the default, uncluttered state).
Published, with unpublished changes "Published with changes" indicator. Subtle left border or background tint.
Validation warning Amber warning icon on the node. Visible regardless of selection. Tooltip shows the problem.
Has annotations Small annotation count indicator (e.g., "12 studies"). Visible in properties panel with full detail.

No version numbers on nodes. Version history is accessed via the properties panel "Version history" action.

Cross-Question Validation Warnings

When editing a question creates a problem for another question:

During editing (live, in the properties panel):

  • Impact preview section appears when changes would affect other questions
  • Shows which other questions are affected and why
  • Example: "Removing option 'Weight drop' will break the display condition on 3 child questions"
  • This is informational -- the change is autosaved as a draft regardless

After saving (persistent, in the tree):

  • Affected nodes show a warning icon
  • Warning persists until the admin fixes the child question's condition
  • Clicking the warning shows: "This question's display condition references option 'Weight drop' which no longer exists on parent question 'TBI model type'"
  • The publish button remains disabled while warnings exist (for the stage containing these questions)

Properties Panel -- Status & Draft Changes

When a published question with draft changes is selected, the properties panel shows:

STATUS
------
Published (last: Mar 15, 2026)     [Version history]

DRAFT CHANGES (unpublished)
----------------------------
Text: "What species?" -> "What animal species is used?"
Options: [Mouse, Rat, Dog] -> [Mouse, Rat, Dog, Cat]

These changes will be published when a stage
containing this question is next published.

                                    [Discard draft changes]

The "Discard draft changes" action reverts to the last published content.

Version History Dialog

Accessible via "Version history" in the properties panel. Shows all published versions:

VERSION HISTORY: "What animal species is used?"
-----------------------------------------------
Mar 15, 2026  Chris    "Added Cat to species list"
  Options: [Mouse, Rat, Dog] -> [Mouse, Rat, Dog, Cat]
                                          [Apply as draft]

Feb 28, 2026  Chris    (initial publication)
  Text: "What animal species is used?"
  Options: [Mouse, Rat, Dog]
                                          [Apply as draft]
  • Each entry shows: date, author, change reason (if provided), and a summary of what changed
  • "Apply as draft" restores that version's content as the current draft. It does NOT revert -- it creates a new draft state that must be published to take effect.
  • The draft changes section in the properties panel then shows the diff between the latest published version and the restored content.
  • The version history notes that the draft was derived from a previous version.

5. Assign View -- Two Visual Dimensions

The Assign view communicates pending changes through two orthogonal visual dimensions, replacing the previous three-indicator design. All assignment changes are autosaved -- indicators show the diff between the published state and the current autosaved draft state.

Dimension 1: Change Status (icon, per-question)

Indicator Meaning
No indicator Identical to what is currently published for this stage
Small amber dot / edit_note Has changes that will be included in next publish -- whether from admin's own edits OR from another stage's publish (merged concept, no distinction in tree)
Warning icon (amber warning) Validation error that blocks publishing

The previous "Updated version available" indicator (blue sync icon for cross-stage publishes) is merged into the generic amber "changed" indicator. The source of changes (own edits vs other-stage publishes) is NOT distinguished in the tree -- that breakdown appears in the publish wizard Step 1 and as a quiet inline notice when entering the Assign view.

Dimension 2: Assignment Changes (row treatment)

Treatment Meaning
Normal row Currently published on this stage, will remain
Left green border + "+" prefix NOT currently published, will be added
Left red border + "-" prefix Currently published, will be removed
Unchecked, no border, muted Not assigned, never has been

Cross-Stage Publish Communication

The distinction between "your edits" and "updates from other stages" is shown:

  • Assign view entry: Quiet inline notice (not a banner): "3 questions were updated when Screening was published on Mar 15."
  • Publish wizard (Step 1): Changes grouped by source -- "Content changes (your edits)" vs "Updated from other stages"
  • Properties panel: On-demand detail when selecting a changed question

Filter Toggle Interaction

The filter toggle should use All / On stage / Not on stage and should be based on the published stage assignment state (not the draft). Unpublished-change overlays (green/red indicators) remain visible on top of that baseline so admins can compare what annotators see now against what will change on next publish.

Annotations Warning on Removal

When a question with annotations is being removed from a stage (red indicator), an additional warning is shown: "This question has N annotations across M studies. Removing it will not delete annotations, but annotators will no longer see it for new sessions."


6. Preview View -- Draft vs Published

Preview Scope Options

The Preview view should offer two preview modes:

Option A: Stage Preview (primary)

  • Select a stage from the dropdown
  • Preview shows the annotation form as it would appear with the current configuration:
  • If the stage has a published SQS: shows the published version by default
  • Toggle to "Preview with pending changes" to see what the form will look like after publishing
  • Pending changes are highlighted (new questions, changed text, removed questions shown as struck-through or absent)

Option B: Full Project Preview (secondary)

  • Shows all questions across all categories without stage filtering
  • Useful for reviewing the complete question set before publishing
  • Available as a separate option: "Preview: [Stage Dropdown] | [Full Project]"

Draft vs Published Toggle

+------------------------------------------------------------------+
| Preview                                                            |
| Stage: [Extraction v]                                              |
|                                                                    |
| Showing: ( ) Published version   (*) With pending changes         |
|                                                                    |
| [Banner: "You are previewing pending changes. 3 questions have     |
|  been modified, 1 added, 0 removed since the last publish."]       |
+------------------------------------------------------------------+

When "With pending changes" is selected:

  • Questions with draft content changes show their DRAFT text/options (not the published version)
  • Questions pending addition to the stage are shown with a subtle "new" indicator
  • The annotation form is fully interactive for testing conditional logic

When "Published version" is selected:

  • Shows exactly what annotators currently see
  • No draft content, no pending assignments

7. Publish Button Placement

Research Summary

CMS best practices (Craft CMS, Payload CMS, Webflow, Drupal) consistently show:

  • The publish action should be contextual -- visible where the user is working
  • Status information (draft/published) should be glanceable at all times
  • The button state should change based on context (new vs published vs has changes)

Decision: Review & Publish on Assign, Publish Stage on Preview

Assign page (primary entrypoint):

One primary action in the action bar:

  • "Review & Publish N changes": Opens the publish wizard from the Assign page. This keeps the main configuration workflow focused and avoids forcing the admin to choose between two publish entry points before they have reviewed the changes.
+------------------------------------------------------------------+
| [Action Bar]                                                       |
| 5 live on stage | 4 pending changes                               |
|                                      [Discard] [Review & Publish 4 changes] |
+------------------------------------------------------------------+

Preview page (secondary entrypoint):

"Publish Stage" button in the action bar. Available when the admin is previewing pending changes. It opens the same publish wizard as the Assign page, but from the preview context.

+------------------------------------------------------------------+
| [Preview Action Bar]                                               |
| Showing pending changes for Extraction                             |
| 2 edited, 1 added, 1 removed                    [Publish Stage]  |
+------------------------------------------------------------------+

Design page: No publish button.

Shows a banner when draft changes exist: "You have draft changes. Go to Assign to configure and publish a stage."

"Publish All Stages" Placement

Less common action. Available in:

  • The Assign view toolbar (overflow menu)
  • The project-level Stage Settings page

8. Pre-Publish Validation

The Publish Button Should Be Disabled When Invalid

The publish button is always visible but disabled with a clear explanation when the configuration is invalid.

+------------------------------------------------------------------+
| [Action Bar -- INVALID STATE]                                      |
|                                                                    |
| [!] Cannot publish: 2 validation errors                           |
|                                                                    |
| 1. "If other, specify" references option "Weight drop" on         |
|    parent "TBI model type" which no longer exists.                 |
|    [Go to question in Design ->]                                   |
|                                                                    |
| 2. "Dose" is a required child of "Drug Name" but "Drug Name"      |
|    is not assigned to this stage.                                  |
|    [Assign "Drug Name" ->]                                         |
|                                                                    |
|                                          [Publish Stage] (disabled) |
+------------------------------------------------------------------+

Validation Runs Continuously

  • Validation is not just a pre-publish check -- it runs continuously as the admin makes changes
  • The action bar updates in real time as questions are assigned/unassigned
  • Errors link directly to the problematic question in the Design view or offer one-click fixes (like assigning a missing parent)

Validation Categories

Category Severity Blocks Publish?
Broken conditional reference (option removed) Error Yes
Missing parent in stage (child assigned without parent) Error Yes
Question with empty text Error Yes
Duplicate option values Error Yes
Question has no options (dropdown/checklist with 0 options) Error Yes
Question has draft changes but no stage assignment Warning No
Outdated AQ version in stage (newer version exists) Info No

9. Cross-Question Validation in Design View

Live Impact Preview

When editing a question in the Design view properties panel, if the change would affect other questions, an impact section appears:

+------------------------------------------------------------------+
| PROPERTIES: "What kind of TBI model is used?"                      |
|                                                                    |
| Options:                                                           |
| [x] Weight drop                                                   |
| [x] Controlled cortical impact          [Remove selected]         |
| [x] Fluid percussion injury                                       |
| [x] Blast injury                                                   |
|                                                                    |
| [!] IMPACT: Removing "Weight drop" will affect 3 questions:       |
| - "From which height (cm)?" -- condition: TBI model = Weight drop  |
| - "Amount of weight used?" -- condition: TBI model = Weight drop   |
| - "Did they attach to skull?" -- condition: TBI model = Weight drop|
|                                                                    |
| These questions will show a validation warning until their         |
| conditions are updated.                                            |
|                                                                    |
|                                    [Discard draft changes] |
+------------------------------------------------------------------+

Draft State Allows Invalid Configurations

  • Draft changes that create broken references are autosaved without blocking
  • Warnings appear on affected nodes in the tree and persist until fixed
  • The publish button is disabled for any stage containing broken questions
  • This allows incremental changes without forcing the admin to fix everything in one session

10. PQS as Implicit vs Explicit Concept

Decision: Keep PQS/SQS Fully Hidden from Users

The ProjectQuestionSet and StageQuestionSet are important data model entities, but they are never surfaced in the UI. The admin thinks in terms of:

  • "I'm designing questions" (Design view)
  • "I'm configuring what a stage shows" (Assign view)
  • "I'm publishing a stage" (Publish action)

They do not need to think about PQS or SQS as separate concepts. Publishing creates new AQ versions, not "PQS versions." The admin publishes "to a stage," not "an SQS."

If identification of a particular publish snapshot is needed, the UI uses sequential labels with timestamps: "Update 3 -- Mar 28, 14:32". Dates alone are ambiguous (multiple publishes per day).

When to Introduce the Concept Explicitly

The project-level question set concept becomes relevant to the user when:

  1. Reconciliation: The reconciled gold-standard annotation set maps to the full project question set.
  2. Cross-project sharing: If questions are shared between projects.
  3. Data export: Exporting the complete question set for a project.

Decision: Introduce the concept in the UI when reconciliation is implemented. For now, it exists in the data model but is not surfaced. The admin's mental model is "stages have questions" -- the project-level aggregation is implicit.


11. Resolved Decisions

Formerly open questions, now resolved through discussion.

RD-1: Publish Button Placement

Decision: Publish on both Assign and Preview pages, but with one primary path. Assign uses a single primary action, "Review & Publish N changes", and Preview exposes a secondary "Publish Stage" action when the admin is reviewing unpublished changes.

RD-2: Preview Scope

Decision: Option B -- Preview shows individual stages (via dropdown) AND a "Full Project (all questions)" option. The full project option includes an info banner: "This shows all questions across all stages. This is not what any individual stage's annotation form looks like."

RD-3: Outdated Versions Require Intentional Update (Corrected)

Decision: SQS updates are never automatic. When a shared question gets a newer AQ version (via another stage's publish), other stages show an "updated version available" indicator on the Assign page — but the admin must intentionally publish that stage to adopt the newer versions.

Rationale: Updating an SQS has downstream consequences for active annotation sessions. The Admin Decision Framework (see PR2398 planning docs) evaluates the impact per-session: carrying forward answers, blanking invalid answers, marking questions for re-answering, handling PAC (Parent Annotation Condition) cascades. This is not something that should happen as a side effect — it requires review and confirmation.

UI implications: The "updated version available" indicator on the Assign page is not just informational — it's a prompt for the admin to review and decide when to publish. The publish ceremony for this stage will include the Admin Decision Framework if there are active sessions.

RD-4: Draft Changes Indicator Granularity

Decision: Option C -- Expandable detail with progressive disclosure. Minimal indicator by default (small dot or icon). Expandable on hover/click to show a summary of what changed.

RD-5: PQS Introduction Timing

Decision: Option A (implicit now), with consideration for Option B (light introduction) later. The PQS is not surfaced to the admin in the current QM UI. It exists in the data model and is created implicitly when a stage publishes. The PQS concept will be introduced explicitly when reconciliation is implemented, as the reconciled gold-standard annotation set maps to the full PQS.

RD-6: Autosave vs Manual Save

Decision: Autosave for all draft changes (Figma/Google Docs model). No manual "Save" action. The only deliberate action is "Publish." Draft safety provided by undo/redo stack (session-level) and periodic automatic snapshots.

RD-7: Version Numbers

Decision: Do not show version numbers (v1, v2, etc.) on question nodes. Show only "Draft" (never published) or "Published with changes" (published, with unpublished changes). Version history with dates/timestamps accessible via properties panel "Version history" dialog. "Apply as draft" action available on previous versions.

RD-8: Review & Publish Confirmation

Decision: "Review & Publish" on the Assign page opens a step-by-step publish wizard. "Publish Stage" on the Preview page also triggers this wizard.

12. Remaining Open Questions

OQ-A: Stage Question Set Version History

Should there be a mechanism in the UI to view previous SQS versions and their differences? If so, where should this live -- in the Assign view, in Stage Settings, or both?

OQ-B: Draft Snapshots Browsing — RESOLVED

Decision: Draft history is accessible from the Design view via a "Draft history" action (separate from published version history). Shows a timeline of GFS-retained snapshots. Admin can restore the entire draft state or a single question. See State Lifecycle > Layer 2: Draft snapshots for full details.

OQ-C: Responsive Design

Minimum node width requirements and the trigger point for focus mode on narrow screens need prototyping. Bottom sheet behaviour for the properties panel on mobile needs design.


13. Prototype Design Decisions Addendum

Additional decisions established during interactive prototyping that augment this specification.

PVD-01: Assign View Filter Semantics

The filter toggle uses three states: All / On stage / Not on stage. These refer to the last published SQS for the selected stage, not the current draft checkbox state. Unpublished-change overlays (green/red indicators) remain visible on the filtered rows so admins can review pending additions and removals against the published baseline. A "Last published: [date]" timestamp is shown next to the filter.

PVD-02: Checkbox Propagation Rules

When a user checks or unchecks a question in the Assign view:

  • Checking a child automatically checks all ancestors up to the root (parent integrity constraint)
  • Unchecking a parent automatically unchecks all descendants
  • Indeterminate state shown when a parent is checked but not ALL of its descendants are checked
  • System questions are always checked and disabled

PVD-03: Change Detection Baseline

Changes in the Assign view are detected by comparing q.checked !== q.published — the draft state vs. the published state. The Discard button is disabled when no changes exist from the published baseline.

PVD-04: Status Icon System for Change Indicators

The Assign view uses icon-only indicators with tooltips (no worded badges) to reduce visual noise:

  • add_circle (green): "Will be added on publish"
  • remove_circle (red): "Will be removed on publish"
  • edit_note (orange): "Has unpublished changes"
  • draft (purple): "Draft — never published"
  • lock (grey): "System question"
  • warning (amber): Validation issue

PVD-05: Preview Mode — Published vs With Changes

The Preview view includes a toggle between "Published" and "With Changes" modes:

  • Published: Shows exactly what annotators currently see. Simulation mode banner.
  • With Changes: Shows how the form will look after publishing. Orange changes banner with counts. Removed questions shown struck-through with danger-coloured outline.

PVD-06: Review Page Progress Indicators

Two segmented progress bars above the study card:

  • Screening bar (primary blue): You screened / Remaining for you / Unavailable (sufficiently screened by others)
  • Annotation bar (success green): You annotated / Remaining for you / Unavailable (sufficiently annotated by others)

Each segment has a distinct visual: solid for "You", light tint for "Remaining", hatched pattern for "Unavailable". Legends with colour keys below each bar.

PVD-07: Version History Property Previews

When browsing version history (both individual question and PQS level), each version entry can be expanded to show the full properties snapshot that would be applied if restored:

  • All fields shown: text, help, type, control (with icon), required, multiple, options list, default checkbox status, parent question, show-when condition
  • Changed fields highlighted in orange for quick identification
  • "Apply as draft" action per version (individual) or "Restore all" / "Restore selected" (PQS level)

PVD-08: Project Question History — Tree with Selective Restore

The PQS version browser (accessible from Design toolbar) shows:

  • Expandable version cards with metadata (date, stage, question count, change summary)
  • Full question tree per version with hierarchy and control type icons
  • Each question row is clickable to expand and preview its properties at that version
  • Checkboxes on non-system questions for selective restore
  • "Restore all as draft" and "Restore selected" buttons with live selection counter

PVD-09: Publish Dialog Structure

The Review & Publish confirmation dialog shows:

  1. Content changes section (questions getting new AQ versions with diffs)
  2. Questions being added to the stage
  3. Questions being removed from the stage
  4. Result card with new PQS/SQS version numbers
  5. Optional change reason text input
  6. "Breaking changes" checkbox for flagging potentially invalidating changes

PDD-10: Change Reason Capture -- Three-Layer Approach

The challenge: by publish time, the admin may have forgotten why they edited a question days or weeks ago. Research across REDCap, Figma, Git, Confluence, and Qualtrics shows that optional single-field reasons at publish time have low adoption (~10-20%), and required fields get low-quality text unless the user has immediate context.

Layer 1: Lightweight inline change notes during editing (Design view)

A small, collapsible 'note' affordance in the properties panel -- a sticky note icon that expands to a one-line text input. When the admin edits a published question (creating draft changes), they can optionally jot a note like 'Added SC route per reviewer feedback.' This note:

  • Is attached to the question's DraftContent on the AnnotationQuestion
  • Auto-timestamped
  • Persists server-side (autosaved like everything else)
  • Is carried forward to the publish dialog as a pre-populated change reason

This captures context at the moment of change without adding friction (optional, inline, no interruption).

Layer 2: Diff-assisted publish dialog (Assign view publish ceremony)

When the admin clicks 'Review and Publish,' the dialog shows:

  • Auto-generated field-level diffs per question ('Options: Oral, IV, IP -> Oral, IV, IP, SC')
  • Any inline notes from Layer 1 pre-populated in per-question change reason fields
  • A batch-level summary field at the top for the overall SQS version reason
  • The admin can edit, add to, or leave these as-is

This is the Git model: you see the diff while writing the commit message. The inline notes are like git add -p annotations, and the publish summary is the commit message.

Layer 3 (future): AI-assisted summaries

Auto-generate a draft change summary from the diffs: 'This publish updates 3 questions: simplified wording on Q1 and Q3, added new route option to Drug Route.' The admin edits/accepts. Only worth implementing after observing Layer 1+2 adoption.

Stage Publish Reason

The stage-level publish reason is the batch-level summary from the publish wizard — it describes the overall intent of the stage update (e.g., 'Updated treatment questions based on protocol amendment v3'). Per-question reasons provide granularity; the stage reason provides narrative. (Internally stored on the SQS version, but this term is not shown to users.)

Implementation Priority

  1. Start with Layer 2 (diff-assisted publish dialog) -- highest-value intervention
  2. Add Layer 1 (inline notes) in the next iteration
  3. Consider Layer 3 (AI summaries) only after observing real usage patterns

PDD-11: Replace with New Version (Replaces Unpublish)

Mental model review decision: The "unpublish" concept is replaced by a forward-only "Replace with new version" action. See mental-model-review-decisions.md, Section 6 for the full specification.

Summary: A unified "Replace with new version" action is available for ALL published questions (with or without annotations). It creates new DraftQuestions linked to the originals via replacesAnnotationQuestionId, removes originals from the draft PQS, and allows full structural editing on the replacements. At publish time, if the originals had annotations, the publish wizard handles annotation migration (Phase 8).

The previous "unpublish" design below is superseded but preserved for reference.


SUPERSEDED — Previous unpublish design (kept for historical reference):

Unpublishing demotes AnnotationQuestions back to DraftQuestion status on the project. It is a project-level design operation with cross-stage consequences.

Precondition: No annotations exist for ANY AQVersion of the affected questions on ANY stage. If annotations exist, unpublishing is blocked.

State Diagram:

                    PUBLISH
  +-----------+   (creates AQ +     +------------------------+
  |  DRAFT    |   AQVersion + PQS   |  PUBLISHED             |
  |           | ------------------> |  (no annotations)      |
  | DraftQ    |                     |  AQ + AQVersion        |
  | on Proj   | <------------------ |  on pmAQ collection    |
  +-----------+    UNPUBLISH        +------------------------+
                   (see below)               |
                                             | Annotator submits
                                             | first annotation
                                             v
                                      +------------------------+
                                      |  PUBLISHED             |
                                      |  (with annotations)    |
                                      |  CANNOT unpublish      |
                                      |  (version only)        |
                                      +------------------------+

Unpublish steps:

  1. Validate: Confirm no annotations exist for any AQVersion of these questions on any stage. If annotations exist, block with error message listing the stages and annotation counts.

  2. For each affected AnnotationQuestion: a. Create a new DraftQuestion on the Project from the latest AQVersion content b. Mark the AnnotationQuestion as status: 'reverted' (NOT deleted -- immutable history preserved) c. The AQVersions remain in the collection as historical records

  3. Update the draft PQS: a. Replace QuestionReferences from type: 'published' to type: 'draft' pointing at the new DraftQuestion IDs

  4. For each Stage that references these questions: a. If the SQS draft contains references to the reverted AQs, remove them from the draft SQS b. Published SQSVersions remain immutable (historical record) c. Show a notification to the admin: 'N questions were unpublished. They have been removed from [Stage X, Stage Y] assignments.'

  5. PQSVersions that referenced the now-reverted AQVersions: a. Remain in history as immutable records b. Marked with status: 'superseded_by_unpublish' for display purposes c. Cannot be restored (the AQVersions they reference are reverted)

Key design decisions:

  • AQVersions are never deleted -- they are marked as reverted
  • PQSVersions are never deleted -- they are marked as superseded
  • The unpublish operation is all-or-nothing per question -- you cannot unpublish a single version, only the entire question
  • Cross-stage impact is clearly communicated before confirmation
  • System questions are excluded from unpublish operations

Unpublish confirmation dialog:

  • List of questions being unpublished
  • List of stages that will be affected
  • Warning: 'These questions will be removed from all stage assignments'
  • If any question has annotations on any stage, the button is disabled with an explanation of which stages have annotations

PDD-12: Cross-Stage Change Detection

Mental model review decision: "Update available" is no longer a separate indicator in the Assign tree. Cross-stage changes are merged into the generic "changed" indicator (amber dot). The breakdown by source (own edits vs other-stage publishes) is shown in the publish wizard Step 1, not in the tree.

Detection algorithm (internal — not surfaced as separate UI state):

For each question Q assigned to this stage:
  1. Get Q's AQVersion from the last published version for this stage
  2. Get Q's latest published AQVersion (from any stage's publish)
  3. If versions differ OR Q has draft changes:
     -> Show "changed" indicator (amber dot) on the question row
  4. To provide context on the source of changes (for publish wizard):
     For each other Stage S in the project:
       If S published a newer version of Q:
         -> Record S.name and publish date for the wizard Step 1 grouping

UI representation:

  • In the Assign tree: A single amber dot (merged "changed" indicator) covers both own edits and other-stage publishes. No separate "update available" icon.
  • Quiet inline notice when entering the Assign view: "N questions were updated when [Stage] was published on [date]."
  • In the publish wizard (Step 1: Review Changes): Changes grouped by source — "Content changes (your edits)" vs "Updated from other stages" — showing stage names, dates, and change reasons.
  • In the properties panel: On-demand detail when a changed question is selected, showing whether the change is from own edits or another stage's publish.

14. Autosave Failure and Offline Resilience

Multi-Layer Connection Detection

SyRF uses SignalR with .withAutomaticReconnect() as the primary connection indicator. This is supplemented by two additional layers:

Layer Detection Method Speed
SignalR Heartbeat-based disconnect/reconnect events Seconds
API failures HTTP interceptor tracks consecutive save failures (>3 = failing) Per-request
Network events navigator.onLine + online/offline events, periodic health endpoint fetch Immediate / 30s

These feed into a centralised ConnectionState:

interface ConnectionState {
  signalR: 'connected' | 'reconnecting' | 'disconnected';
  api: 'healthy' | 'failing';
  network: 'online' | 'offline';
  effective: 'connected' | 'degraded' | 'offline';  // computed
}

Visual Indicators

Save status (properties panel header, always visible during editing):

State Indicator
No pending changes Nothing shown
Saving sync spinner + 'Saving...'
Saved check + 'Saved' (fades after 3s)
Save failed, retrying warning amber + 'Retrying...'
Offline, queued locally cloud_off amber + 'Saved locally'
Failed, manual retry error red + 'Save failed' + [Retry] button

Connection banner (top of content area, only when degraded/offline):

State Banner
Degraded Amber: 'Connection unstable. Changes are being saved locally.'
Offline Amber: 'You are offline. Changes are saved locally and will sync when reconnected.'
Reconnected Green: 'Reconnected -- syncing changes...' (auto-dismisses after 5s)

Offline Editing Policy

Editing is NOT blocked when offline. All draft changes are written to IndexedDB immediately (before attempting the API call). On reconnect, the queue is flushed in order.

Rationale: Every best-in-class tool (Google Docs, Figma, Notion) and every research domain tool (REDCap Mobile) allows continued editing offline. Blocking edits because of a transient network interruption while a researcher is mid-annotation is unacceptable.

Local Persistence

IndexedDB (not localStorage) is used for the offline queue:

User edits form
  -> Debounced change detection (200ms)
  -> Write to IndexedDB queue (ALWAYS, regardless of connection)
  -> Attempt API save
       Success -> mark queue entry as synced
       Failure -> entry stays in queue, exponential backoff retry
On reconnect -> flush queue oldest-first

A beforeunload handler warns the user if they attempt to close the browser with unsynced changes.

Concurrent Edit Detection

Annotations are per-user-per-study, so multi-user conflicts are rare. Same-user-two-tabs is detected via the BroadcastChannel API:

const channel = new BroadcastChannel('syrf-question-edits');
// Posts questionId + tabId on edit
// Warns if another tab is editing the same question

Implementation Priority

  1. Save status indicator (low complexity, high value)
  2. beforeunload warning (trivial, high value)
  3. Connection banner with centralised ConnectionStateService (low complexity)
  4. IndexedDB offline queue (medium complexity, high value)
  5. Tab awareness via BroadcastChannel (low complexity)