Skip to content

Question Management

A dedicated admin workspace for creating, editing, organizing, and versioning annotation questions across their full lifecycle -- from draft design through stage activation to controlled version evolution.

Overview

Question Management is the project administrator's primary tool for defining what annotators are asked. It provides a three-tab interface (Design, Assign, Preview) where administrators build annotation questions as drafts, assign them to stages, and activate them for use. Once activated, questions follow a versioning model that preserves every change as an immutable snapshot while keeping existing annotations intact.

This feature replaces the current mutable-question model where edits silently rewrite history. Instead, administrators get a clear two-phase lifecycle: free-form drafting followed by controlled, auditable versioning. The result is a question management system that supports protocol evolution without compromising traceability.

Problem Statement

SyRF's current question management has three fundamental limitations:

  1. Editing locks after first answer. Once any annotator answers a question, the entire question is locked. Administrators cannot fix a typo, add a missing dropdown option, or improve help text without creating an entirely new question -- even when the change is minor and non-breaking.

  2. No version history. When a question is changed (before it gets locked), the edit happens in-place. There is no record of what the question looked like before. Past annotations silently change meaning because there is no link between "the question as it was" and "the answer as it was recorded."

  3. No stage-level question configuration history. Stages reference questions by ID only, with no record of which questions (at which versions, in what order) were active when annotations were collected. It is impossible to reconstruct the exact annotation form an annotator saw at any point in time.

These limitations prevent SyRF from meeting the traceability and reproducibility standards expected by journals, funders, and regulatory bodies for systematic reviews.

Solution

Question Management introduces a structured lifecycle for annotation questions:

  • Draft Questions (DQs) are fully mutable design-time objects. Administrators iterate freely -- changing text, options, types, parent-child relationships -- with no versioning overhead and no downstream impact.

  • Annotation Questions (AQs) are the committed, production entities created when a stage is activated. Their structural identity (data type, parent, group-as-single) is fixed at birth. Content properties (text, options, help text, answer filters) can be edited, but each edit creates a new immutable AQVersion.

  • Question Set Versions (QSVs) are immutable snapshots of which question versions, in what order, are assigned to a stage. Any change to a stage's question assignments creates a new QSV, preserving the complete history of what annotators were asked.

This gives administrators the flexibility to evolve their annotation protocol while giving the system the immutability guarantees it needs for audit trails, reconciliation, and cross-project data aggregation.

Scope

In Scope

  • Draft Question creation and editing in the QM Design tab
  • All six question types: boolean, select, checklist, text, numeric, autocomplete
  • Question options, help text, and answer option filter configuration
  • Parent-child question hierarchy and group-as-single toggle
  • Drag-and-drop reordering of questions
  • Stage assignment via the QM Assign tab (dual-tree interface)
  • Stage activation converting DQs to AQs with initial AQVersions
  • Content editing of activated questions with automatic version creation
  • pendingChanges buffer for server-side auto-save of in-progress edits
  • Question Set and QSV composition with parent integrity enforcement
  • Version history viewer with side-by-side diff comparison
  • Version badges on question cards
  • Admin decision framework for handling active sessions when QSV changes
  • Preview tab showing the exact annotation form annotators will see
  • System question handling (auto-included, version-managed by system)
  • Feature flag (newQuestionManagement) for controlled rollout
  • Migration of existing questions to v1 AQVersions

Out of Scope / Future

  • Cross-project question sharing -- Import-as-reference, fork-to-customize, community catalogue, and organisation-level libraries are v2 features (SHARE-01 through SHARE-06)
  • Advanced question reorganization wizards -- Copy, Copy & Disable, Copy & Delete workflows (AQM-02)
  • AQV update impact assessment wizard -- Options A/B/C for affected annotations (AQM-01)
  • System questions as a CAMARADES-curated QuestionSet -- Remodelling hardcoded system questions (AQM-03)
  • Annotation Form v2 -- The rebuilt form with virtual scroll and per-question auto-save is a separate feature (FORM-01 through FORM-08)
  • Reconciliation workflow -- Authority determination, reconciliation dashboard, and reconciler UI are separate features (RECON-01 through RECON-17)

User Workflows

Creating Draft Questions

The project administrator opens the Question Management page and navigates to the Design tab. This is the workspace for building questions before any stage is activated.

Steps:

  1. Click "Add Question" to create a new Draft Question
  2. Choose a question type (see Question Types below)
  3. Enter the question text -- the wording annotators will see
  4. Configure type-specific settings:
  5. For select and checklist: add answer options (labels and optional values)
  6. For autocomplete: define the option source
  7. For all types: optionally add help text and answer option filters
  8. Optionally set a parent question to create a conditional child (the child is shown only when specific parent answers are selected)
  9. Optionally enable "group as single" to combine parent and child questions into a single visual unit on the annotation form

Draft Questions are saved to the project entity. They have no version history, no downstream dependencies, and no restrictions on what can be changed. Every property is fully mutable during the draft phase.

Organizing Questions

The Design tab displays questions as a hierarchical tree. Administrators organize questions using:

  • Drag-and-drop reordering -- Move questions up and down within the tree to control display order. Drop targets indicate whether the question will be inserted as a sibling (same level) or as a child (nested under another question).
  • Parent-child hierarchy -- Drag a question onto another question to make it a child. Child questions inherit conditional display logic from their parent.
  • Category grouping -- Questions are grouped by category (e.g., Study Design, Outcomes, Risk of Bias) within the tree.

The tree supports expand/collapse for nested hierarchies and provides visual feedback during drag operations (drop zone highlighting, insertion indicators).

Assigning to Stages

The administrator switches to the Assign tab to configure which questions appear in each stage.

Steps:

  1. Select a target stage from the stage selector (stage must be disabled)
  2. The interface shows two panels: unassigned questions (left) and assigned questions (right)
  3. Drag questions from unassigned to assigned, or use the Assign/Unassign buttons
  4. Reorder assigned questions to set display order
  5. System questions for data extraction stages are automatically included and cannot be unassigned

Parent integrity is enforced during assignment: if a child question is assigned to a stage, all of its ancestors are automatically included. The administrator sees the question count update as questions are added or removed.

Changes on the Assign tab are saved independently from the Design tab. The system warns the administrator before navigating away with unsaved assignment changes.

Previewing Before Activation

The Preview tab renders the exact annotation form that annotators will see for a selected stage. This includes:

  • Question rendering in display order
  • Conditional logic visualization (parent-child show/hide behavior)
  • Correct control types for each question (dropdown, checkbox, text input, etc.)
  • Help text displayed alongside questions
  • Answer option filters applied

Preview operates on the current state of assigned Draft Questions. It allows the administrator to verify the annotation experience before committing to activation.

Activating a Stage

When the administrator is satisfied with the question design and assignment, they enable the stage. Activation triggers the following system operations:

  1. DQ-to-AQ conversion -- Each Draft Question assigned to the stage is converted to an Annotation Question. The DraftId is replaced with a permanent AqId. The DQ ceases to exist.
  2. Initial AQVersion creation -- Each new AQ receives its first AQVersion (v1) containing the current text, options, help text, and answer filters.
  3. Identity property freeze -- The AQ's structural properties (data type, parent question, group-as-single) are fixed at this point and cannot be changed afterward.
  4. QSV composition -- The system creates the stage's first Question Set Version (QSV) -- an immutable, ordered list of the AQVersionIds assigned to the stage.
  5. Stage opens for annotation -- The stage becomes available to annotators.

Activation is a one-way operation. Once a stage is activated, its questions follow the versioning model described below.

Editing Activated Questions

After activation, the administrator can still modify question content, but changes follow a controlled versioning process.

What can be changed (content properties):

Property Description
Question text The wording annotators see
Options Answer choices for select, checklist, and autocomplete types
Help text Guidance shown alongside the question
Answer option filters Conditional option restrictions based on parent answers

What cannot be changed (identity properties):

Property Reason
Data type Changing from boolean to string would invalidate all existing answers
Parent question Changing hierarchy restructures entity subtrees and breaks annotation tree references
Group-as-single Changes the fundamental display structure of parent-child groups

To change an identity property, the administrator must create a new question.

The editing workflow:

  1. Open the question in the Design tab -- a version badge shows the current version (e.g., "v2")
  2. Edit the content properties. Changes are auto-saved to the pendingChanges buffer on the AQ entity (server-side, survives browser changes)
  3. When ready, click "Save as New Version" -- the system commits pendingChanges as a new AQVersion with an incremented version number
  4. The administrator can optionally record a change reason explaining why the edit was made
  5. The new AQVersion is available for assignment to stages via QSV composition

The pendingChanges buffer allows the administrator to make multiple edits before committing them as a single version. Pending changes do not affect annotators -- only committed AQVersions are referenced by QSVs and shown on annotation forms.

Updating a Stage's Question Set

After creating a new AQVersion (or adding/removing questions), the administrator updates the stage's question configuration:

  1. Open the Assign tab and select the stage
  2. Modify the assigned questions (add, remove, reorder, or upgrade to a newer AQVersion)
  3. The system composes a new QSV -- an immutable snapshot of the updated configuration
  4. If the stage has active annotation sessions under the previous QSV, the administrator is prompted with the Admin Decision Framework

Version History

The administrator can view the complete version history for any activated question:

  1. Select a question in the Design tab
  2. Open the version history panel
  3. View a timeline of all versions: version number, who created it, when, and the change reason
  4. Select any two versions for side-by-side diff comparison
  5. Diffs highlight exactly what changed: added/removed options, modified text, updated help text

Version history provides the audit trail required for systematic review traceability. Every change to every question is recorded with attribution and timestamp.

Version Badges

Each question card in the Design and Assign tabs displays a version badge (e.g., "v3") showing the current version number. This provides at-a-glance awareness of:

  • How many times a question has been edited since activation
  • Whether a question is on its initial version (v1) or has evolved
  • Which version is currently assigned to a given stage (in the Assign tab)

Admin Decision Framework

When a stage's QSV changes, the system versions forward by creating new ASVs on affected sessions. The new ASV's shape depends not only on the QSV diff but also on each session's specific answers, because parent-child conditionality means the same QSV change can affect different sessions differently.

See Annotation Versioning — Resolved Question Set and Parent Annotation Condition for the formal definitions referenced below.

Why the QSV diff alone is insufficient

A simple AQ-level diff (added, removed, updated) is a necessary starting point but does not fully determine the impact on each session. Parent-child conditionality means an annotation's presence in the new ASV depends on its entire ancestor chain:

  • An option removed from a root question may cascade to exclude an entire subtree of child annotations
  • A change to an ancestor's answer filters may cause previously-hidden questions to become visible (or vice versa)
  • Whether a carried-forward answer is valid depends not just on the AQVersion change but on whether the annotation satisfies the Parent Annotation Condition (PAC) in the new ASV

The system must therefore evaluate each session individually, reconstructing the new ASV top-down from root annotations using PAC.

Breaking change metadata on AQVersions

Each AQVersion carries a BreakingChange: boolean flag set by the admin when creating the version. This records the admin's intent about whether the change invalidates existing answers.

Transitivity: When a QSV transition jumps across multiple AQVersions (e.g., v2 → v5), the transition is breaking if any intermediate version was marked breaking. The system computes this automatically.

Contextual breaking: Even non-breaking changes can be breaking for specific sessions. For example, a "non-breaking" option addition is generally safe, but if a different version removes an option that a specific annotator had selected, their answer becomes invalid. The BreakingChange flag is the admin's general classification; the reconstruction algorithm performs actual break detection per-session by checking whether each carried-forward answer remains valid against the new AQVersion.

The admin is warned at version creation time that the flag represents intent, not a guarantee — the system will detect contextual breaks automatically during QSV transitions.

ASV reconstruction algorithm

New ASVs are built top-down from root annotations, evaluating PAC at each level of the question tree:

  1. Sort the new QSV's AQVersions in tree order (roots first, then children by depth)
  2. For each AQVersion q in the sorted list: a. Evaluate PAC(q) against the partially-built new AVMap. If PAC fails, skip q and its entire subtree b. If PAC passes, classify the AQ change and determine the AV:
Classification Condition AV in new ASV Metadata
Carried forward AQVersion identical in both QSVs, existing AV exists Existing AV reference None
Carried forward (blank) AQVersion identical, no existing AV (unanswered) Blank AV None
Migrated (non-breaking) AQVersion updated, not marked breaking, answer still valid against new AQVersion New AV created with carried-forward answer provenance: {sourceAVId, reason: "non-breaking-update"}
Auto-detected breaking AQVersion updated, not marked breaking, but answer invalid against new AQVersion (e.g., selected option no longer exists) Blank AV flags: {needsReanswering: true, reason: "answer-invalid-after-update"}
Breaking update AQVersion updated, marked breaking (or transitive breaking) Blank AV flags: {needsReanswering: true, reason: "breaking-update"}
New question AQ newly added to the QSV Blank AV flags: {needsAnswering: true, reason: "new-question"}
Newly visible AQ existed in old QSV but was excluded from old ASV because PAC failed (ancestor change now includes it) Blank AV flags: {needsAnswering: true, reason: "newly-visible-from-ancestor-change"}
Removed AQ was in old QSV but not in new QSV, or AQ no longer satisfies PAC due to ancestor changes Excluded from new AVMap Non-breaking; Annotation and AV still exist, just not referenced
  1. After processing all AQVersions, the new ASV's status is determined by the admin's choices (see below) and whether any blanks exist

The new ASV records ResolvedAQVersionIds — the set of AQVersionIds that satisfied PAC — as a materialized field for queryability.

Admin decision flow

Rather than choosing a single option, the admin answers a series of questions that provide granular control over the transition:

Step 1 — Scope

  • Leave ALL sessions on old QSV? (pin everything, only new sessions use new QSV) → If yes, done
  • Leave complete sessions on old QSV? → If yes, only process incomplete sessions; if no, process all

Step 2 — Non-breaking transitions

For sessions where the reconstruction produces no breaking changes (no blank AVs needed):

  • Auto-create new ASVs? → If no, leave these sessions on old QSV
  • For migrated AVs (non-breaking AQVersion updates): carry forward answers to new AVs, or leave blank for annotator to re-answer?
  • Inherit completion status from previous ASV, or mark all as incomplete for review?

Step 3 — Breaking transitions

For sessions where the reconstruction produces breaking changes (one or more blank AVs):

  • Update to new QSV anyway? → If yes, create new ASV marked incomplete with breaking AVs flagged. If no, leave on old QSV

Step 4 — Confirmation

The system presents a summary:

  • N sessions will get new ASVs (X complete, Y incomplete)
  • N sessions will remain on old QSV
  • Per-AQ breakdown of carried, migrated, breaking, new, and removed annotations
  • For each breaking AV: which sessions are affected and why

The admin reviews and confirms. The decision and all parameters are recorded as part of the QSV assignment metadata for audit purposes.

Question Types

Type Control Options Required Answer Format Notes
Boolean Toggle / Yes-No No true / false Simplest type. Used for binary decisions.
Select Dropdown Yes (2+ options) Single option value Annotator picks exactly one option from a list.
Checklist Checkbox group Yes (2+ options) Array of option values Annotator picks zero or more options.
Text Text input No Free-text string For qualitative or unstructured responses.
Numeric Number input No Integer or decimal Accepts integer or decimal values.
Autocomplete Autocomplete input Yes (option source) Single or multiple values Type-ahead search against a defined option list.

All question types support:

  • Help text -- Guidance text shown alongside the question to assist annotators
  • Answer option filters -- Conditional logic that restricts which options are available based on the parent question's answer
  • Optional/Required -- Whether the annotator must provide an answer
  • Parent-child relationships -- Conditional display based on parent answer selection

Data Model

Draft Annotation Question (DraftAQ)

Lives on the Project aggregate. Fully mutable. No version history.

DraftAQ
  DraftId: Guid              -- temporary, replaced by AqId on activation
  DataType: string            -- mutable during draft phase
  ParentId: Guid?             -- mutable during draft phase
  GroupAsSingle: boolean      -- mutable during draft phase
  Text: string                -- mutable
  Options: List<AnswerOption> -- mutable
  HelpText: string?           -- mutable
  AnswerFilters: List<AnswerFilter>  -- mutable
  Category: QuestionCategory  -- mutable

DraftAQs are destroyed on activation. They do not survive beyond the draft phase.

Annotation Question (AQ)

Lives in the pmAnnotationQuestion collection. Identity properties are immutable after activation. Content properties are versionable through AQVersions.

AnnotationQuestion
  Id: Guid                    -- permanent, assigned at activation
  Scope: System | Organisation | Researcher | Project
  OwnerId: Guid               -- SystemId | OrgId | InvestigatorId | ProjectId
  Category: QuestionCategory
  DataType: AnswerType         -- IDENTITY: immutable after activation
  ParentQuestionId: Guid?      -- IDENTITY: immutable after activation
  GroupAsSingle: boolean       -- IDENTITY: immutable after activation
  DerivedFrom: QuestionId?     -- null if original, source QuestionId if forked
  PendingChanges: {...} | null -- mutable auto-save buffer for in-progress edits
  CurrentVersionNumber: int
  Versions: List<AQVersion>    -- append-only

AQVersion

Immutable content snapshot. Embedded within the AQ document.

AQVersion
  Id: Guid                    -- AQVersionId, globally unique
  VersionNumber: int           -- sequential: 1, 2, 3, ...
  QuestionText: string
  Options: List<AnswerOption>
  HelpText: string?
  AnswerFilters: List<AnswerFilter>
  CreatedAt: DateTime
  CreatedBy: Guid
  ChangeReason: string?

QuestionSet (QS)

Lives in the pmQuestionSet collection. Represents a versioned collection of question versions assigned to a stage.

QuestionSet
  Id: Guid                    -- QuestionSetId, stable across versions
  Scope: System | Organisation | Researcher | Project
  OwnerId: Guid
  Name: string
  Description: string?
  DerivedFrom: QuestionSetId?
  CurrentVersionNumber: int
  Versions: List<QuestionSetVersion>  -- append-only

QuestionSetVersion (QSV)

Immutable snapshot of ordered AQVersionId references. Embedded within the QS document.

QuestionSetVersion
  Id: Guid                    -- QSVId, globally unique
  VersionNumber: int
  AQVersionIds: OrderedList<Guid>  -- the question versions, in display order
  CreatedAt: DateTime
  CreatedBy: Guid
  ChangeReason: string?

Parent integrity constraint: All ancestors (via the ParentQuestionId hierarchy) of any AQVersion in a QSV must also be present in the same QSV. This is enforced at QSV composition time. If a child question is included, the system automatically includes all of its ancestor questions. This means cross-stage question overlap is structural and expected -- a parent question shared between stages will appear in both stages' QSVs.

Property Classification Summary

Category Properties Mutability Rule
Identity AqId, DataType, ParentQuestionId, GroupAsSingle Set at activation, immutable forever
Content Text, Options, HelpText, AnswerFilters Versionable via AQVersion. Edited through pendingChanges buffer, committed to create new AQVersion.
Derived CurrentVersionNumber, Versions[] Managed by the system

API

Draft Question CRUD

Method Path Description
POST /api/projects/{projectId}/draft-questions Create a new DraftAQ
GET /api/projects/{projectId}/draft-questions List all DraftAQs for the project
GET /api/projects/{projectId}/draft-questions/{draftId} Get a specific DraftAQ
PUT /api/projects/{projectId}/draft-questions/{draftId} Update a DraftAQ (any property)
DELETE /api/projects/{projectId}/draft-questions/{draftId} Delete a DraftAQ

Stage Assignment

Method Path Description
PUT /api/projects/{projectId}/stages/{stageId}/draft-assignments Set which DraftAQs are assigned to a disabled stage
GET /api/projects/{projectId}/stages/{stageId}/draft-assignments Get current DraftAQ assignments for a stage

Stage Activation

Method Path Description
POST /api/projects/{projectId}/stages/{stageId}/activate Activate stage: converts assigned DQs to AQs, creates initial AQVersions and first QSV, enables stage

Annotation Question Versioning

Method Path Description
GET /api/projects/{projectId}/questions List all AQs for the project
GET /api/projects/{projectId}/questions/{questionId} Get AQ with current version
PUT /api/projects/{projectId}/questions/{questionId}/pending Update pendingChanges (auto-save buffer)
POST /api/projects/{projectId}/questions/{questionId}/versions Commit pendingChanges as a new AQVersion
GET /api/projects/{projectId}/questions/{questionId}/versions List all AQVersions for a question
GET /api/projects/{projectId}/questions/{questionId}/versions/{versionNumber} Get a specific AQVersion

Question Set Version Management

Method Path Description
GET /api/projects/{projectId}/stages/{stageId}/question-config Get current QSV for the stage
PUT /api/projects/{projectId}/stages/{stageId}/question-config Update stage question configuration (creates new QSV). Includes impact choices if active sessions exist.
GET /api/projects/{projectId}/stages/{stageId}/question-config/history Get QSV history for the stage

Migration Strategy

Existing production data is migrated additively during Phase 7. No data is deleted or moved.

Question backfill:

  • Each existing AnnotationQuestion on a project becomes an AQ with CurrentVersionNumber = 1
  • The current field values (text, options, type, etc.) are captured as the initial AQVersion (v1) -- this is accurate, since questions have never been versioned before
  • Scope defaults to Project, OwnerId defaults to ProjectId

Stage configuration backfill:

  • For each stage, the existing HashSet<Guid> AnnotationQuestions is converted into an initial QSV (v1)
  • Question order is derived from existing display order
  • EffectiveFrom is set to the stage creation date

Backward compatibility:

  • The existing question management UI continues to work behind the newQuestionManagement feature flag
  • API consumers see the same data structures with additional optional fields
  • Rollback is clean: remove the new fields with a $unset operation

Success Criteria

# Requirement Acceptance Criterion
1 QM-01 PA can create Draft Questions in the QM Design tab with all six question types, saved on the project entity
2 QM-02 PA can edit DQ text, options, help text, and answer option filters freely while in draft state
3 QM-03 PA can assign DQs to a disabled stage via the QM Assign tab using drag-and-drop or button actions
4 QM-04 PA can preview assigned DQs on the QM Preview tab, seeing the exact form annotators will use
5 QM-05 PA can enable a stage, triggering DQ-to-AQ activation (DQs converted to AQs with initial AQVersions, first QSV created, stage enabled for annotation)
6 QM-06 AQ structural properties (parent, data type, group-as-single) are fixed at activation and cannot be modified
7 QM-07 AQ content properties (text, options, help text, answer option filters) are versionable -- editing creates a new AQVersion while preserving all previous versions
8 QM-08 QuestionSet/QSV model implemented -- immutable snapshots of ordered AQVersionId lists with version history
9 QM-09 Parent integrity constraint enforced at QSV composition time (all ancestors of any included AQVersion must also be included)
10 QM-10 Project Question Library maintains a flat manifest of AQVersionId references available to the project
11 QM-11 PA can view question version history with side-by-side diff comparison between any two versions
12 QM-12 PA can create a new AQVersion when editing an activated question via a versioning wizard showing the version badge
13 QM-13 Admin decision framework presents options (pin, invalidate, require re-annotation) when a new QSV is assigned to a stage with active sessions
14 QM-14 Version badge displayed on each question card showing the current version number
Document Relationship
Annotation Versioning Design Authoritative source for DraftAQ lifecycle (D37), property classification (D38), pendingChanges (D40), and QSV composition (D43)
Design Decisions Authoritative reference for all 50 design decisions (D1-D50). Sections 1 and 11 cover the AQ entity model and lifecycle patterns.
Product Overview PO-facing description of the Question Management Platform, including phased delivery (Phases 3 and 5)
Annotation Form v2 Rebuilt annotation form that renders QSV-configured questions with per-question auto-save
Reconciliation Workflow that compares answers collected against specific AQVersions to produce gold-standard records
Requirements Requirements QM-01 through QM-14 with phase traceability

Implementation Appendices

⚠️ Precedence note: The appendices below were merged from the closed PR #2325 branch (created 2026-02-09) to preserve implementation detail not carried forward during spec restructuring. The product-level spec above is authoritative for design intent. Where appendix content contradicts the spec above or the Design Decisions (D1–D50), the higher-precedence document wins. In particular:

  • The data model sections below use the original embedded AnnotationQuestion with _versionHistory approach. The authoritative model uses separate pmAnnotationQuestion and pmQuestionSet collections with Identity + Immutable Versions (D37–D40). See the Data Model section in the spec above.
  • The Angular UI component inventory and unmerged PR references remain valuable for implementation — they describe existing codebase assets to extend, not rebuild.
  • The API specification endpoints are illustrative. Final API design should follow the patterns established in the spec above.
  • The acceptance criteria are comprehensive and should be used as a starting point for Phase 4–6 planning, updated to reflect the current data model.

Existing Implementation Inventory

A substantial Question Management v2 implementation already exists in the codebase behind the newQuestionManagement feature flag. This must be the starting point — not a greenfield build.

Feature Flag

  • Name: newQuestionManagement
  • Environment variable: SYRF__FeatureFlags__NewQuestionManagement
  • Helm value: featureFlags.newQuestionManagement
  • Default: false
  • Definition: src/services/web/src/app/generated/feature-flags.types.ts
  • Selector: selectNewQuestionManagement in ngrx state
  • When flag is enabled, nav menu shows "Question Management" (icon: add_task) routing to ['admin', 'questions']
  • Legacy "Question Design" route (['admin', 'question-design']) remains for flag-off state
  • Located in: src/services/web/src/app/project/project-nav/project-nav.component.ts

Component Inventory

Component Location Completeness Notes
QuestionManagementComponent question-management/question-management.component.ts 100% Tab container (Design/Assign/Preview), computed signals for child questions
Routes question-management/question-management.routes.ts 100% All 3 tabs routed with guards for stage-based routing
DesignComponent question-management/design/design.component.ts 95% Tree visualization, Material tree, tab group, loading indicator
DesignStore question-management/design/design.store.ts (28KB) 100% Full ngrx signal store: question tree, drag-drop, focus, view state, conditional parent answers, boolean/options conditionals, validation
DragDropFeature design/annotation-question-tree-drag-drop.feature.ts (23KB) 100% Complete drag-drop implementation: start/over/leave/drop, dropzone positioning, tree reorganisation, sibling vs child insertion
TreeFeature design/annotation-question-tree.feature.ts 100% Flat→tree conversion, category grouping, node maps with ancestor/descendant tracking
QuestionsFeature design/annotation-questions.feature.ts 100% Generic signal feature: allQuestionsByCategoryMap, allQuestionMap, named prefixes for multiple question sets
QuestionNodeComponent design/question-node/ 90% Individual node: edit/delete/copy actions, nested rendering, validation feedback. Delete/copy are stubs
AssignComponent assign/assign.component.ts 90% Stage selector, question counts, Assign/Unassign button
AssignStore assign/assign.store.ts (31KB) 100% Dual-tree UI (unassigned↔assigned), drag-drop between trees, batch operations, tree reset, computed counts
StageAssignComponent assign/stage-assign/ 100% Questions for specific stage with guard
PreviewComponent preview/preview.component.ts 30% Stage selector only — preview rendering not implemented
StagePreviewComponent preview/stage-preview/ 30% Skeleton — needs significant work
EditComponent edit/edit.component.ts 80% Question property editing UI
EditFormValidations edit/edit.form-validations.ts 80% Validation rules for question creation/editing
HybridComponent hybrid/hybrid.component.ts Legacy Example showing hybrid/legacy integration

Backend (C#) — Complete Models, No New API Endpoints

The backend models are fully implemented but there are no new API endpoints for the v2 question management operations. Current CRUD goes through the existing AnnotationQuestionController:

  • AnnotationQuestion.cs — Full entity with Target, Options, Categories, SubquestionIds
  • Target.cs — Conditional parent logic with schema versioning (v0/v1)
  • AnnotationAnswerTallyStore — Tracks annotation counts per question (for locked question checks)

What's Complete vs. What's Missing

Complete (can be reused): - Tree-based question visualization with drag-and-drop - ngrx signal stores for design and assign workflows - Question creation/editing UI (form fields, options, conditionals) - Dual-tree assign/unassign interface - Feature flag infrastructure - Navigation integration - Route configuration with guards

Missing (needs implementation): - Backend API persistence — store methods don't call backend APIs (stubs) - Question versioning (no QuestionVersion model or UI) - Version history / diff viewer - Stage question config management (composing and versioning question configs per stage) - Preview tab rendering - Delete/copy actions in design store - Undo/redo - Comprehensive test coverage

Unmerged PR Code Worth Salvaging

PR Key Code Reuse Value
#1649 validation-utils.ts (+550 lines) — comprehensive validation for parent-child question relationships, option consistency, conditional state High — core validation logic needed for both design page and backend validation service
#1649 new-option/new-option.component.ts — dedicated component for adding/editing question options High — cleaner than current inline option editing
#1649 form-model.directive.ts, form-model-group.directive.ts — enhanced template-driven form directives with dirty tracking Medium — patterns useful for form v2
#1564 edit-text-dialog/, edit-options-dialog/ — wizard components for editing locked questions High — needed for the "edit answered question" workflow (Discussion #1646 requirement)
#1586 locked-question.svg + lock icon integration on assign page Medium — visual asset and pattern reusable directly
#1631 design.store.ts additions for preview state Low — store has been rewritten since; preview approach may need rethinking

Angular UI Components

Strategy: Extend Existing v2, Don't Rebuild

The existing question-management/ component tree is the foundation. New features should be added to the existing components, not built as parallel replacements.

Signal forms mandate: All form-driven interactions (question creation/editing, stage question config, edit wizards) must use Angular 21 @angular/forms/signals instead of ReactiveFormsModule or template-driven forms. See Angular 21 Signal Forms section below for detailed patterns.

Design Tab (Existing — Extend)

Existing: src/services/web/src/app/project/project-admin/question-management/design/

Extend with: - Version indicator on each question node showing current version number - "Save as New Version" button when editing an existing question (currently saves in-place) - Version diff sidebar when editing — shows what changed vs. previous version - Backend persistence — connect DesignStore methods to API (currently stubs) - Delete/Copy — implement stub methods in QuestionManagementComponent

Assign Tab (Existing — Extend)

Existing: src/services/web/src/app/project/project-admin/question-management/assign/

Extend with: - Version selector per question when adding to stage (default: latest) - Config preview showing exactly what annotators will see - Config history showing previous stage question configurations

Incorporate from unmerged PRs: - Lock icon for system questions (PR #1586 pattern) - Placeholder text improvements (PR #1649)

Preview Tab (Existing — Complete)

Existing: src/services/web/src/app/project/project-admin/question-management/preview/

This tab is only ~30% complete and needs: - Question rendering matching the annotation form layout - Conditional logic visualization (show/hide based on parent answers) - Sample answer display for each control type - Version-aware rendering from the stage's embedded StageQuestionConfig

Incorporate from unmerged PRs: - Design page preview approach (PR #1631 — adapted for new store structure)

New: Question Version History

Add as a new view/panel within the Design tab: - QuestionHistoryComponent: Timeline of versions with expand/collapse - Side-by-side diff between selected versions - Filter by date, user

New: Edit Wizards for Locked Questions

From Discussion #1646 and PR #1564 — questions that have already been answered need special handling: - EditTextDialog — wizard for changing question text on answered questions - EditOptionsDialog — wizard for modifying options on answered questions - Entry point: overflow menu on locked question nodes (padlock icon) - Wizard must warn about implications and create a new version

Angular 21 Signal Forms

Shared technology decision: Both the Question Management UI and the Annotation Form v2 must use Angular 21 experimental signal-based forms (@angular/forms/signals). This ensures consistency and avoids maintaining two different form paradigms.

All form-driven interactions in the QM UI must use signal forms instead of ReactiveFormsModule or template-driven forms:

Design Tab — Question Editing Form

The question creation/editing form (currently in EditComponent) must use form() with schema validation:

import { form, required, validate, schema } from '@angular/forms/signals';

interface QuestionEditModel {
  text: string;
  questionType: string;
  controlType: string;
  category: string;
  optional: boolean;
  multiple: boolean;
  helpText: string;
  options: QuestionOptionModel[];
}

@Component({ ... })
export class EditComponent {
  readonly model = signal<QuestionEditModel>(defaultQuestion());

  readonly questionForm = form(this.model, (fields) => {
    schema(fields.text, required(), validate(v =>
      v.length > 500 ? { maxLength: true } : null
    ));
    schema(fields.questionType, required());
    schema(fields.controlType, required());
    schema(fields.category, required());
  });
}

Template bindings use [formField]:

<mat-form-field>
  <mat-label>Question Text</mat-label>
  <input matInput [formField]="questionForm.controls.text" />
  @if (questionForm.controls.text.errors()?.required) {
    <mat-error>Question text is required</mat-error>
  }
</mat-form-field>

<mat-select [formField]="questionForm.controls.controlType">
  @for (ct of controlTypes; track ct.value) {
    <mat-option [value]="ct.value">{{ ct.label }}</mat-option>
  }
</mat-select>

Assign Tab — Stage Question Config Form

The version selector for assigning questions to a stage uses signal forms:

interface StageQuestionConfigModel {
  selectedQuestions: Array<{ questionId: string; versionNumber: number; position: number }>;
}

readonly configForm = form(this.configModel, (fields) => {
  schema(fields.selectedQuestions, required(), validate(qs =>
    qs.length === 0 ? { minQuestions: true } : null
  ));
});

Edit Wizards — Dialog Forms

The EditTextDialog and EditOptionsDialog from PR #1564 must be reimplemented with signal forms:

// edit-text-dialog.component.ts
interface EditTextModel {
  newText: string;
  reason: string;
}

readonly editForm = form(this.model, (fields) => {
  schema(fields.newText, required(), validate(v =>
    v === this.originalText() ? { unchanged: true } : null
  ));
  schema(fields.reason, required());
});

// Submit creates a new question version
onSubmit() {
  submit(this.editForm, async (value) => {
    await this.questionApi.createVersion(this.questionId(), {
      text: value.newText,
      changeReason: value.reason,
    });
  });
}

Validation Utilities (from PR #1649)

The validation-utils.ts (+550 lines) from PR #1649 contains parent-child validation logic. This must be adapted to work with signal forms by converting validators to the validate() / applyWhen() schema pattern:

// Before (PR #1649 — template-driven):
// Custom directive validators + manual form control checks

// After (signal forms):
// Use validate() for synchronous rules
schema(fields.options, validate(options =>
  hasDuplicateOptions(options) ? { duplicateOptions: true } : null
));

// Use applyWhen() for conditional parent-child rules
applyWhen(
  () => this.model().controlType === 'dropdown',
  fields.options,
  validate(opts => opts.length < 2 ? { minOptions: true } : null)
);

Backend Implementation Notes

Validation

The Backend Annotation Validation Service must be extended to:

  1. Validate question versions are self-consistent (e.g., options exist for dropdown types)
  2. Validate StageQuestionConfig composition (no duplicate questions, valid ordering, referenced versions exist)
  3. Validate that annotations reference valid question version numbers

Indexes

No new collections are introduced — all versioning data is embedded in the existing pmProject aggregate. The existing indexes on pmProject are sufficient. No additional indexes required for question management.

See Data Model & Migration for document size monitoring queries that serve as the primary operational concern for embedded data.

Events (MassTransit)

public record QuestionVersionCreated(Guid ProjectId, Guid QuestionId, int VersionNumber);
public record StageQuestionConfigUpdated(Guid ProjectId, Guid StageId, int ConfigVersion);

API Specification (HTTP Examples)

Question CRUD

# Create a new question (generates version 1)
POST /api/projects/{projectId}/questions
Content-Type: application/json
{
  "text": "What was the sample size?",
  "questionType": "integer",
  "controlType": "textbox",
  "category": "Study",
  "optional": false,
  "description": "Total number of subjects in the study",
  "helpText": "Count all subjects, including those lost to follow-up"
}
→ 201 Created { "id": "...", "versionNumber": 1 }

# Create a new version of an existing question
POST /api/projects/{projectId}/questions/{questionId}/versions
Content-Type: application/json
{
  "text": "What was the total sample size (all groups)?",
  "helpText": "Updated: count all subjects across all experimental groups"
}
→ 201 Created { "versionNumber": 2, "previousVersion": 1 }

# Get all versions of a question
GET /api/projects/{projectId}/questions/{questionId}/versions
→ 200 OK [{ "versionNumber": 1, ... }, { "versionNumber": 2, ... }]

# Get a specific version
GET /api/projects/{projectId}/questions/{questionId}/versions/{versionNumber}
→ 200 OK { "versionNumber": 2, "text": "...", "createdBy": "...", "createdAt": "..." }

Stage Question Configuration

# Update stage question configuration (creates a new config version)
PUT /api/projects/{projectId}/stages/{stageId}/question-config
Content-Type: application/json
{
  "questions": [
    { "questionId": "...", "versionNumber": 2, "position": 0 },
    { "questionId": "...", "versionNumber": 1, "position": 1 }
  ]
}
→ 200 OK { "configVersion": 3 }

# Get current stage question configuration
GET /api/projects/{projectId}/stages/{stageId}/question-config
→ 200 OK {
    "version": 3,
    "questions": [...],
    "effectiveFrom": "2026-03-01T00:00:00Z"
  }

# Get configuration history for a stage
GET /api/projects/{projectId}/stages/{stageId}/question-config/history
→ 200 OK [
    { "version": 1, "questions": [...], "effectiveFrom": "2026-01-01" },
    { "version": 2, "questions": [...], "effectiveFrom": "2026-02-15" }
  ]

Acceptance Criteria Summary

Versioning & Stage Configuration (New)

  • Editing a question creates a new version; previous versions remain immutable in _versionHistory
  • Stage question configs can be composed with drag-to-reorder
  • Updating a stage config appends the previous config to QuestionConfigHistory
  • Historical annotations resolve to the correct question version via QuestionVersionNumber
  • Full audit log for all question management operations
  • RBAC: only Project Admins can manage questions
  • API validates all business rules server-side
  • System questions handled automatically (no manual versioning)
  • Migration preserves all existing annotation data

MVP Feature Parity (from Discussion #1646 — must not regress)

  • Users cannot unassign system questions related to data extraction if extraction is enabled on a stage
  • All user-created questions are usable by the annotation form (no invalid combinations)
  • Invalid parent-child states are prevented — if parent options change, child conditional options must be re-validated
  • Validation errors shown when: question text too long, question text empty, duplicate options, options don't match data type
  • When parent gets a new option, child questions show validation requiring user to select/deselect the new option (asterisk + indeterminate checkbox)
  • Questions that have been answered cannot be edited (locked state); edit wizards provide controlled modification
  • Automatic check on whether a question has been answered (annotation tally)
  • If annotations are deleted, questions become editable again
  • Preview displays correct control type based on question settings
  • Assign page is intuitive for naive users (placeholder text, clear action labels)
  • Drag-and-drop works reliably and performantly
  • Dirty check before leaving assign page with unsaved changes

Locked Question Wizards (from Discussion #1516 / PR #1564)

  • Edit text wizard available for answered questions
  • Edit options wizard available for answered questions
  • Wizard entry point via overflow menu (padlock icon)
  • Wizard creates a new question version (does not mutate existing)

Angular 21 Signal Forms (@angular/forms/signals)

  • Question creation/editing form uses form() + [formField] — no ReactiveFormsModule
  • All form validation uses schema-based required(), validate(), applyWhen()
  • Edit wizards (text, options) use signal forms with submit() for typed submission
  • Assignment form (date picker, version selector) uses signal forms
  • PR #1649 validation logic adapted to validate() / applyWhen() schema pattern
  • Conditional parent-child rules use applyWhen() for reactive enable/disable