Skip to content

Annotation Questions Business Logic

Overview

This document provides a comprehensive analysis of the business logic governing annotation questions in SyRF. Annotation questions are the foundation of data extraction in systematic reviews, allowing researchers to define structured questions and capture annotations from study papers.

Critical Finding: A significant portion of domain logic currently resides exclusively in the frontend code, creating challenges for:

  • Database seeding (invalid question structures can be created)
  • Data integrity (backend cannot enforce all business rules)
  • Maintainability (logic is duplicated or inconsistent)
  • API consumers (rules are not enforced at the API level)

Terminology Note

For precise terminology definitions, see the Terminology Glossary in the formal specification. Key terms include:

  • Unit = A named instance created by answering a Label Question (e.g., "Aspirin 100mg" is a Treatment unit)
  • Label Question = System question that creates units
  • Control Question = Boolean question indicating whether a unit is a "control" (experimental control group)
  • Lookup Question = Question that cross-references units from another category

Table of Contents

  1. Core Concepts
  2. Stage Data Extraction Settings
  3. Question Visibility and Form Display
  4. Annotation Storage
  5. System Annotation Question GUIDs
  6. Category-Specific Rules
  7. Question Hierarchy Rules
  8. Lookup Questions
  9. Conditional Logic
  10. Frontend-Only Business Logic (Problem Area)
  11. Recommendations

Core Concepts

Question Types

Property Values Description
questionType string, integer, decimal, boolean Data type of the answer
controlType textbox, dropdown, checkbox, radio, checklist, autocomplete UI control for input
system true/false Whether it's a system-defined question
labelQuestion true/false Creates named instances (units)
annotationLookup true/false References previously created instances
root true/false Has no parent question
multiple true/false Allows multiple answer instances

Categories

┌─────────────────────────────────────────────────────────────────────┐
│                        ANNOTATION CATEGORIES                         │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  ┌─────────┐     Non-unit categories (flat structure)               │
│  │  Study  │     - Simple questions with no label/unit concept       │
│  └─────────┘                                                         │
│                                                                      │
│  ┌─────────────────────────────────────────────────────────────┐    │
│  │        Unit-Based Categories (hierarchical structure)        │    │
│  │                                                              │    │
│  │  ┌─────────────────────┐  ┌─────────────────────┐           │    │
│  │  │  Disease Model      │  │     Treatment       │           │    │
│  │  │  Induction          │  │                     │           │    │
│  │  │  ───────────────    │  │  ───────────────    │           │    │
│  │  │  Has Control Q      │  │  Has Control Q      │           │    │
│  │  └─────────────────────┘  └─────────────────────┘           │    │
│  │                                                              │    │
│  │  ┌─────────────────────┐  ┌─────────────────────┐           │    │
│  │  │  Outcome            │  │     Cohort          │           │    │
│  │  │  Assessment         │  │                     │           │    │
│  │  │  ───────────────    │  │  ───────────────    │           │    │
│  │  │  No Control Q       │  │  No Control Q       │           │    │
│  │  │  Has special        │  │  Has lookup Qs      │           │    │
│  │  │  outcome questions  │  │                     │           │    │
│  │  └─────────────────────┘  └─────────────────────┘           │    │
│  │                                                              │    │
│  │  ┌─────────────────────┐                                    │    │
│  │  │    Experiment       │                                    │    │
│  │  │  ───────────────    │                                    │    │
│  │  │  No Control Q       │                                    │    │
│  │  │  References Cohorts │                                    │    │
│  │  └─────────────────────┘                                    │    │
│  └─────────────────────────────────────────────────────────────┘    │
│                                                                      │
│  ┌─────────┐     Hidden category (not displayed)                    │
│  │ Hidden  │     - System questions that shouldn't be shown         │
│  └─────────┘                                                         │
└─────────────────────────────────────────────────────────────────────┘

Stage Data Extraction Settings

The Critical Control: Stage.Extraction Boolean

The Stage.Extraction boolean is the master switch that determines whether data extraction (and its associated system questions) is enabled for a stage. This single setting fundamentally changes which questions are available to annotators.

Backend (Stage.cs:93-96):

public ImmutableHashSet<Guid> AllStageAnnotationQuestions =>
    ImmutableHashSet.CreateRange(AnnotationQuestions).Union(Extraction
        ? AnnotationQuestion.SystemQuestionIds
        : ImmutableHashSet<Guid>.Empty);

Frontend (annotation-question.selectors.ts:133-154):

export const selectAnnotationQuestionsForCurrentStage = createSelector(
  selectAnnotationQuestionsForCurrentProject,
  selectCurrentStage,
  (questionsForProject, stage): IAnnotationQuestion[] | null => {
    // ... filtering logic ...
    return stage.extraction
      ? _.union(sysAnnotationQuestions, stageQuestions)
      : stageQuestions;
  }
);

Core Logic:

stage.extraction Questions Shown Use Case
true System questions + Custom questions Full data extraction with structured outcome capture
false Custom questions only Simple screening or custom annotation without structured extraction

System Questions = Data Extraction Questions

The 18 system questions (defined in AnnotationQuestion.cs:365-389) enable structured data extraction. These questions are only shown when stage.extraction = true.

Category System Questions Purpose
Experiment Label Name experiments for grouping
Cohort Label, Disease Models (lookup), Treatments (lookup), Outcomes (lookup), Number of Animals Define experimental groups and link to other units
Disease Model Induction Label, Control Define disease models with control/non-control indicator
Treatment Label, Control Define treatments with control/non-control indicator
Outcome Assessment Label, Average Type, Error Type, Units, Greater Is Worse, PDF Graphs Capture quantitative outcome data with statistical metadata
Hidden PDF References Internal references for document linking

Factory Pattern: System questions are created via factory methods in AnnotationQuestion.cs:

private static Dictionary<Guid, Func<Guid, Project, AnnotationQuestion>> SystemQuestionCreators =
    new()
    {
        {ExperimentLabelGuid, CreateExperimentLabelQuestion},
        {CohortLabelQuestionGuid, CreateCohortLabelQuestion},
        {DiseaseModelInductionLabelGuid, CreateDiseaseModelInductionLabel},
        {TreatmentLabelGuid, CreateTreatmentLabel},
        {OutcomeLabelGuid, CreateOutcomeLabel},
        // ... 18 total system questions
    };

public static List<Guid> SystemQuestionIds =>
    __systemQuestionIds ?? (__systemQuestionIds = SystemQuestionCreators.Select(p => p.Key).ToList());

Stage Configuration in the User Guide

In the SyRF application, the stage "data extraction" checkbox corresponds to the Stage.Extraction boolean. When users enable data extraction for a stage:

  1. The annotation form expands to include all unit-based categories (Experiment, Cohort, Disease Model, Treatment, Outcome)
  2. Annotators can create structured units and link them together
  3. Outcome data can be captured for meta-analysis

Question Visibility and Form Display

Question Visibility Flow

┌─────────────────────────────────────────────────────────────────────┐
│                    QUESTION VISIBILITY FLOW                          │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  Stage Configuration                                                 │
│       │                                                              │
│       ▼                                                              │
│  ┌─────────────────────┐                                            │
│  │ stage.extraction    │                                            │
│  │ = true/false?       │                                            │
│  └─────────────────────┘                                            │
│       │                                                              │
│       ▼                                                              │
│  ┌─────────────────────────────────────────────────────────────┐    │
│  │  Backend: Stage.AllStageAnnotationQuestions                  │    │
│  │  Frontend: selectAnnotationQuestionsForCurrentStage          │    │
│  └─────────────────────────────────────────────────────────────┘    │
│       │                                                              │
│       ├──── extraction=true ────►  System Qs + Custom Qs            │
│       │                                                              │
│       └──── extraction=false ───►  Custom Qs only                   │
│                                                                      │
│       ▼                                                              │
│  ┌─────────────────────────────────────────────────────────────┐    │
│  │  Annotation Form                                             │    │
│  │  - Questions grouped by Category (tabs)                      │    │
│  │  - Each category shows its relevant questions                │    │
│  │  - Hierarchy maintained (parent → children)                  │    │
│  └─────────────────────────────────────────────────────────────┘    │
│       │                                                              │
│       ▼                                                              │
│  ┌─────────────────────────────────────────────────────────────┐    │
│  │  Annotations stored in Study.ExtractionInfo.Annotations      │    │
│  │  Each annotation links back to QuestionId                    │    │
│  └─────────────────────────────────────────────────────────────┘    │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

Form Display: Category Tabs

When data extraction is enabled, the annotation form organizes questions into category tabs:

  1. Study - Always available, contains project-level questions
  2. Disease Model Induction - Define disease models (only when extraction=true)
  3. Treatment - Define treatments (only when extraction=true)
  4. Outcome Assessment - Define outcome measures (only when extraction=true)
  5. Cohort - Group disease models, treatments, and outcomes (only when extraction=true)
  6. Experiment - Group cohorts into experiments (only when extraction=true)

Frontend Selector Logic

The selectAnnotationQuestionsForCurrentStage selector (annotation-question.selectors.ts:133-154) performs the filtering:

export const selectAnnotationQuestionsForCurrentStage = createSelector(
  selectAnnotationQuestionsForCurrentProject,
  selectCurrentStage,
  (questionsForProject, stage): IAnnotationQuestion[] | null => {
    if (!stage) return null;

    const questionMap = _.keyBy(questionsForProject, (q) => q.id);
    const sysAnnotationQuestions = questionsForProject.filter((q) => q.system);
    const projectAnnQuesIds = questionsForProject.map((q) => q.id);

    // Filter to questions assigned to this stage, maintaining order
    const stageQsOrdered = projectAnnQuesIds.filter((qid) =>
      stage.annotationQuestions.includes(qid)
    );
    const stageQuestions = stageQsOrdered.map((qid) => questionMap[qid]);

    // The critical decision: include system questions only if extraction enabled
    return stage.extraction
      ? _.union(sysAnnotationQuestions, stageQuestions)
      : stageQuestions;
  }
);

Annotation Storage

Storage Architecture

Annotations are stored in the Study aggregate, specifically within the ExtractionInfo container. This design keeps all extraction-related data together within the study context.

Study
└── ExtractionInfo
    ├── Annotations[]       ← All annotation answers
    ├── Sessions[]          ← Annotation work sessions
    └── OutcomeData[]       ← Quantitative extracted data

Annotation Class Hierarchy

The Annotation class (Annotation.cs) is an abstract base class with typed subclasses for different answer types:

Annotation (abstract)
├── BoolAnnotation          - stores bool? Answer
├── StringAnnotation        - stores string? Answer
├── IntAnnotation           - stores int? Answer
├── DecimalAnnotation       - stores decimal? Answer
├── BoolArrayAnnotation     - stores List<bool> Answer
├── StringArrayAnnotation   - stores List<string> Answer
├── IntArrayAnnotation      - stores List<int> Answer
└── DecimalArrayAnnotation  - stores List<decimal> Answer

Key Annotation Properties

Property Type Purpose
Id Guid Unique annotation identifier
StudyId Guid Links to parent Study
StageId Guid Which stage this was annotated in
AnnotatorId Guid Who created the annotation
QuestionId Guid Links to AnnotationQuestion
Question string Question text (denormalized for display)
Root bool Is this a root-level annotation?
ParentId Guid? Parent annotation for hierarchical answers
Children List\<Guid> Child annotation IDs
Reconciled bool Is this a reconciliation annotation?

ExtractionInfo Container

The ExtractionInfo class (ExtractionInfo.cs) manages all extraction data:

public class ExtractionInfo : ISupportInitialize
{
    public List<Annotation> Annotations { get; private set; } = new();
    public List<AnnotationSession> Sessions { get; private set; } = new();
    public List<OutcomeData> OutcomeData { get; private set; } = new();

    public Study Study { get; set; } = null!;
}

Data Flow: Session Submission

When an annotator submits their work, the AddSessionData() method (ExtractionInfo.cs:50-64) handles the upsert:

public void AddSessionData(Guid investigatorId, SessionSubmissionDto sessionSubmissionDto,
    IEnumerable<Guid> stageQuestionIds, bool includesDataExtraction)
{
    // 1. Upsert annotations for the session
    AddAnnotations(Study.Id, Study.ProjectId, sessionSubmissionDto.Id,
        sessionSubmissionDto.Annotations, sessionSubmissionDto.StageId, stageQuestionIds,
        investigatorId, sessionSubmissionDto.Status,
        sessionSubmissionDto.Reconciliation);

    // 2. If stage has data extraction, also store outcome data
    if (includesDataExtraction)
    {
        AddOutcomeData(sessionSubmissionDto.OutcomeData, investigatorId,
            Study.ProjectId, sessionSubmissionDto.StageId,
            sessionSubmissionDto.Reconciliation);
    }
}

Session Tallies

ExtractionInfo also provides session tallies for tracking annotation progress:

public IEnumerable<SessionTally> SessionTallies =>
    Sessions.GroupBy(ans => ans.StageId).Select(gp =>
        new SessionTally(gp.Key,
            gp.Count(ses => !ses.Reconciliation),                              // Total sessions
            gp.Count(ses => !ses.Reconciliation && ses.Status == Completed),   // Completed
            gp.Any(ses => ses.Reconciliation),                                 // Has reconciliation
            gp.Any(ses => ses.Reconciliation && ses.Status == Completed)       // Reconciliation complete
        )
    );

Linking Questions to Annotations

The critical link between the question configuration and stored data is the QuestionId property on Annotation. This allows:

  1. Form Display: Render the correct control type for each question
  2. Data Retrieval: Find all answers for a specific question across studies
  3. Export: Map annotation answers back to question text and category
  4. Validation: Ensure annotations match expected question types

The Purpose of Data Extraction

Why Enable Data Extraction?

The purpose of enabling Stage.Extraction = true is to:

  1. Show Unit Categories - Make Experiment, Cohort, Disease Model, Treatment, and Outcome Assessment categories visible in the annotation form
  2. Enable Unit Creation - Allow annotators to create units (disease models, treatments, outcomes) with labels
  3. Enable Unit Linking - Allow Cohorts to link to Disease Models, Treatments, and Outcomes via lookup questions
  4. Enable Experiment→Cohort Linking - Allow Experiments to link to their Cohorts
  5. Enable OutcomeData Entry - Once the hierarchy is linked, the UI shows the OutcomeData entry interface

Unit Creation vs. Linking (Critical Distinction)

Units are created FIRST, then linked. This is NOT a containment hierarchy—it's a reference hierarchy:

  1. Create Disease Models, Treatments, and Outcomes independently in their respective tabs
  2. Link them together via lookup questions in Cohort and Experiment tabs
  3. Enter OutcomeData only after the full linking chain is established

Data Extraction Workflow

┌─────────────────────────────────────────────────────────────────────┐
│              DATA EXTRACTION WORKFLOW (Order of Operations)          │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  1. ENABLE DATA EXTRACTION                                           │
│     └─► Stage Settings → Check "Data Extraction" checkbox            │
│                                                                      │
│  2. CREATE UNITS (in any order, but before linking)                  │
│     ┌──────────────────────────────────────────────────────────┐    │
│     │  Disease Model Tab:                                       │    │
│     │    └─► Create disease model labels (e.g., "MCAO")         │    │
│     │    └─► Mark if it's a control (e.g., "Sham" = control)    │    │
│     │                                                           │    │
│     │  Treatment Tab:                                           │    │
│     │    └─► Create treatment labels (e.g., "Aspirin 100mg")    │    │
│     │    └─► Mark if it's a control (e.g., "Vehicle" = control) │    │
│     │                                                           │    │
│     │  Outcome Assessment Tab:                                  │    │
│     │    └─► Create outcome labels (e.g., "Infarct Volume")     │    │
│     │    └─► Set: Average Type, Error Type, Units, Greater Is   │    │
│     │        Worse                                              │    │
│     └──────────────────────────────────────────────────────────┘    │
│                                                                      │
│  3. LINK UNITS VIA COHORT                                            │
│     ┌──────────────────────────────────────────────────────────┐    │
│     │  Cohort Tab:                                              │    │
│     │    └─► Create cohort label (e.g., "Treatment Group A")    │    │
│     │    └─► Select Disease Models (lookup)                     │    │
│     │    └─► Select Treatments (lookup)                         │    │
│     │    └─► Select Outcomes (lookup)                           │    │
│     │    └─► Enter Number of Animals                            │    │
│     └──────────────────────────────────────────────────────────┘    │
│                                                                      │
│  4. LINK COHORTS TO EXPERIMENT                                       │
│     ┌──────────────────────────────────────────────────────────┐    │
│     │  Experiment Tab:                                          │    │
│     │    └─► Create experiment label                            │    │
│     │    └─► Select Cohorts (lookup)                            │    │
│     └──────────────────────────────────────────────────────────┘    │
│                                                                      │
│  5. ENTER OUTCOME DATA                                               │
│     ┌──────────────────────────────────────────────────────────┐    │
│     │  OutcomeData UI appears for each:                         │    │
│     │    Experiment → Cohort → Outcome combination              │    │
│     │                                                           │    │
│     │  For each combination, enter:                             │    │
│     │    └─► TimePoints (time, average, error)                  │    │
│     │    └─► Optionally link to PDF graphs                      │    │
│     └──────────────────────────────────────────────────────────┘    │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

Unit Reference Hierarchy

┌─────────────────────────────────────────────────────────────────────┐
│                    UNIT REFERENCE HIERARCHY                          │
│                                                                      │
│  Units are created independently, then REFERENCED via lookup Qs     │
│                                                                      │
│                    ┌─────────────────────┐                          │
│                    │    EXPERIMENT       │                          │
│                    │  (experimentLabel)  │                          │
│                    └─────────┬───────────┘                          │
│                              │ REFERENCES via: experimentCohorts    │
│                              ▼                                       │
│                    ┌─────────────────────┐                          │
│                    │      COHORT         │                          │
│                    │   (cohortLabel)     │                          │
│                    └─────────┬───────────┘                          │
│                              │ REFERENCES via lookup questions:     │
│        ┌─────────────────────┼─────────────────────┐                │
│        │                     │                     │                │
│        ▼                     ▼                     ▼                │
│ ┌───────────────┐   ┌─────────────────┐   ┌─────────────────┐      │
│ │ DISEASE MODEL │   │   TREATMENT     │   │ OUTCOME         │      │
│ │ (created in   │   │ (created in     │   │ (created in     │      │
│ │  DM tab)      │   │  Treatment tab) │   │  Outcome tab)   │      │
│ └───────────────┘   └─────────────────┘   └─────────────────┘      │
│        ▲                     ▲                     ▲                │
│        │                     │                     │                │
│        └─────── cohort       cohort                cohort ──────────┘
│                 Disease      Treatments            Outcomes          │
│                 Models                                               │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

Control Question Clarification

IMPORTANT: The "Control" question in Disease Model and Treatment categories asks:

"Is this specific unit a control procedure?"

Examples:

  • Disease Model: "Sham surgery" would have Control = true (it's the control procedure)
  • Disease Model: "MCAO" would have Control = false (it's the actual disease model)
  • Treatment: "Vehicle" would have Control = true (it's the control treatment)
  • Treatment: "Aspirin 100mg" would have Control = false (it's the actual treatment)

This is NOT asking "Is this the control group?" - that's determined by the combination of units in a Cohort.


OutcomeData

Overview

OutcomeData is the quantitative data extracted from studies—the actual numerical results that enable meta-analysis. It represents the end goal of the data extraction workflow: capturing time-series outcome measurements with proper statistical metadata.

OutcomeData Model

Backend (OutcomeData.cs):

public class OutcomeData : Entity<Guid>
{
    public Guid StageId { get; }
    public Guid OutcomeId { get; }        // Links to Outcome unit
    public Guid CohortId { get; }         // Links to Cohort unit
    public Guid ExperimentId { get; }     // Links to Experiment unit
    public Guid InvestigatorId { get; }
    public Guid ProjectId { get; }
    public bool Reconciled { get; }
    public bool GreaterIsWorse { get; }   // From Outcome Assessment system question
    public string Units { get; }          // From Outcome Assessment system question
    public string AverageType { get; }    // From Outcome Assessment system question
    public string ErrorType { get; }      // From Outcome Assessment system question
    public int NumberOfAnimals { get; }   // From Cohort system question
    public List<TimePoint> TimePoints { get; }
    public Guid? GraphId { get; }         // Optional PDF graph reference
}

Frontend (outcome-data.entity.ts):

export interface IOutcomeData extends IStudyChild {
  stageId: string;
  outcomeId: string;       // Links to Outcome unit
  cohortId: string;        // Links to Cohort unit
  experimentId: string;    // Links to Experiment unit
  investigatorId: string;
  projectId: string;
  reconciled: boolean;
  greaterIsWorse: boolean;
  units: string;
  averageType: string;
  errorType: string;
  timePoints: ITimePoint[];
  numberOfAnimals: number;
  graphId: string | null;
}

TimePoint ValueObject

Backend (TimePoint.cs):

public class TimePoint : ValueObject<TimePoint>
{
    public double Time { get; }
    public double Average { get; }
    public double Error { get; }
}

TimePoint Field Semantics

Field Meaning Example
Time Time point of measurement (units depend on study) 24 (e.g., 24 hours post-treatment)
Average Central tendency value - either Mean or Median 45.3 (e.g., infarct volume in mm³)
Error Variability measure - SD, SEM, IQR, or CI 12.1 (e.g., standard deviation)

Important: The interpretation of Average and Error depends on the Outcome Assessment system questions:

  • Average is Mean if averageType = "Mean", Median if averageType = "Median"
  • Error is SD/SEM if averageType = "Mean", IQR if averageType = "Median"

OutcomeData Field Mapping

This table shows which system questions populate which OutcomeData fields:

OutcomeData Field Source System Question Category GUID
units Outcome Units Outcome Assessment 66eb1736-a838-4692-a78b-96b0671a377c
averageType Average Type Outcome Assessment 3a287115-5000-4d3f-8c41-7c46fae9adcf
errorType Error Type Outcome Assessment 8dbea59f-54d2-4e41-87e7-fde9e73a72d5
greaterIsWorse Greater Is Worse Outcome Assessment 45351e04-47b2-4785-9a72-713284e917b8
numberOfAnimals Number of Animals Cohort 83caa64f-86a1-4f6e-a278-ebbd25297677
graphId PDF Graphs (lookup) Outcome Assessment 016278e8-7e60-40d4-9568-d7fa42670c32
experimentId Links to Experiment (from annotation form) -
cohortId Links to Cohort (from annotation form) -
outcomeId Links to Outcome (from annotation form) -

Note: numberOfAnimals comes from the Cohort category, not Outcome Assessment. This allows different animal numbers per cohort.

Frontend UI Components

Component Purpose
outcome-data.component.ts Displays outcome data entry interface
data-extraction-form.component.ts Dialog for detailed timepoint entry
annotation-unit.component.ts Unit linking logic
annotation-form.service.ts Form conversion and submission

OutcomeData Display Conditions

The OutcomeData entry UI only appears AFTER:

  1. An Experiment is created with a label
  2. A Cohort is created and linked to the Experiment
  3. An Outcome is created and linked to the Cohort

This ensures all required references (ExperimentId, CohortId, OutcomeId) are available before data entry.

Backend Storage Flow

  1. API Endpoint: ReviewController.SubmitSession
  2. Service Layer: Converts DTO to domain objects
  3. Domain: ExtractionInfo.AddOutcomeData() stores the data
  4. Storage: Study.ExtractionInfo.OutcomeData collection

System Annotation Question GUIDs

These GUIDs are hardcoded in both frontend and backend and must remain synchronized.

Label Questions (Create Named Instances)

Category Question GUID
Disease Model Induction Disease Model Label bdb6e257-5a08-42ef-aad0-829668679b0e
Treatment Treatment Label b02e3072-74f0-44e0-a468-f472b3b09991
Outcome Assessment Outcome Label dbe2720c-2e08-4f47-bcd0-3fe4ae8b8c7f
Cohort Cohort Label 62c852ad-3390-48a4-ac13-439bf6b6587f
Experiment Experiment Label 7c555b6e-1fb6-4036-9982-c09a5db82ace

Control Questions (Is Control Procedure?)

Category Question GUID Parent
Disease Model Induction Model Control b18aa936-a4c6-446b-ac98-88ac38930878 Disease Model Label
Treatment Treatment Control d04ec2d7-3e10-4847-9999-befe7ee4c454 Treatment Label

Lookup Questions (Reference Other Units)

Category Question GUID References
Cohort Cohort Disease Models ecb550a5-ed95-473f-84bf-262c9faa7541 Disease Model Labels
Cohort Cohort Treatments a3f2e5bb-3ade-4830-bb66-b5550a3cc85b Treatment Labels
Cohort Cohort Outcomes 12ecd826-85a4-499a-844c-bd35ea6624ad Outcome Labels
Experiment Experiment Cohorts e7a84ba2-4ef2-4a14-83cb-7decf469d1a2 Cohort Labels
Outcome Assessment PDF Graphs 016278e8-7e60-40d4-9568-d7fa42670c32 PDF References

Outcome Assessment Questions

Question GUID Parent
Average Type 3a287115-5000-4d3f-8c41-7c46fae9adcf Outcome Label
Error Type 8dbea59f-54d2-4e41-87e7-fde9e73a72d5 Outcome Label
Greater Is Worse 45351e04-47b2-4785-9a72-713284e917b8 Outcome Label
Units 66eb1736-a838-4692-a78b-96b0671a377c Outcome Label

Other System Questions

Question GUID Category
PDF References 7ee21ff9-e309-4387-8d30-719201497682 Hidden
Cohort Number of Animals 83caa64f-86a1-4f6e-a278-ebbd25297677 Cohort

Category-Specific Rules

Study Category

Structure: Flat (no label question) Custom Question Placement: Root level only (no parent required)

Study
├── [Custom Question 1] (root: true, target: null)
├── [Custom Question 2] (root: true, target: null)
└── [Custom Question N]...
    └── [Child Question] (conditional on parent answer)

Disease Model Induction Category

Structure: Unit-based with Control Question System Questions: Label → Control Custom Question Placement: Must be children of Control Question

Disease Model Induction
└── Label Question (system, root, labelQuestion: true)
    └── Control Question (system, boolean checkbox)
        ├── [Custom Questions - Control=true]
        │   └── (target.parentId = controlGuid, conditionalParentAnswers.targetParentBoolean = true)
        └── [Custom Questions - Control=false]
            └── (target.parentId = controlGuid, conditionalParentAnswers.targetParentBoolean = false)

CRITICAL RULE (Frontend-Only): First-level custom questions in this category MUST:

  1. Have target.parentId = b18aa936-a4c6-446b-ac98-88ac38930878 (modelControl)
  2. Have conditionalParentAnswers set to either true or false (or null for both)

Nested custom questions parent to other custom questions in the same category (not to modelControl).

Treatment Category

Structure: Unit-based with Control Question (identical to Disease Model Induction) System Questions: Label → Control Custom Question Placement: Must be children of Control Question

Treatment
└── Label Question (system, root, labelQuestion: true)
    └── Control Question (system, boolean checkbox)
        ├── [Custom Questions - Control=true]
        └── [Custom Questions - Control=false]

CRITICAL RULE (Frontend-Only): First-level custom questions in this category MUST:

  1. Have target.parentId = d04ec2d7-3e10-4847-9999-befe7ee4c454 (treatmentControl)
  2. Have conditionalParentAnswers set to either true or false (or null for both)

Nested custom questions parent to other custom questions in the same category (not to treatmentControl).

Outcome Assessment Category

Structure: Unit-based with special system questions System Questions: Label → (Average Type, Error Type, Units, Greater Is Worse, PDF Graphs) Custom Question Placement: Must be children of Label Question

Outcome Assessment
└── Label Question (system, root, labelQuestion: true)
    ├── Average Type (system, dropdown: Mean/Median)
    │   └── Error Type (system, dropdown with conditional options)
    │       ├── [SD, SEM] - shown when Average = Mean
    │       └── [IQR] - shown when Average = Median
    ├── Units (system, textbox)
    ├── Greater Is Worse (system, checkbox)
    ├── PDF Graphs (system, lookup, hidden)
    └── [Custom Questions]
        └── (target.parentId = outcomeLabel)

Cohort Category

Structure: Unit-based with lookup questions System Questions: Label → (Disease Models Lookup, Treatments Lookup, Outcomes Lookup, Number of Animals) Custom Question Placement: Must be children of Label Question

Cohort
└── Label Question (system, root, labelQuestion: true)
    ├── Disease Models (system, lookup → Disease Model Labels)
    ├── Treatments (system, lookup → Treatment Labels)
    ├── Outcomes (system, lookup → Outcome Labels)
    ├── Number of Animals (system, integer textbox)
    └── [Custom Questions]
        └── (target.parentId = cohortLabel)

Experiment Category

Structure: Unit-based with cohort lookup System Questions: Label → Cohorts Lookup Custom Question Placement: Must be children of Label Question

Experiment
└── Label Question (system, root, labelQuestion: true)
    ├── Cohorts (system, lookup → Cohort Labels)
    └── [Custom Questions]
        └── (target.parentId = experimentLabel)

Question Hierarchy Rules

Parent-Child Relationships

  1. Root questions have target: null
  2. Child questions have target.parentId pointing to parent question ID
  3. SubquestionIds on parent questions list child question IDs
  4. Bidirectional linking is maintained automatically

Rules Enforced by Backend

Rule Enforcement Location
Cannot change parent after creation ✅ Backend Project.UpsertCustomAnnotationQuestion()
Cannot update system questions ✅ Backend Project.UpsertCustomAnnotationQuestion()
Deleting parent deletes children ✅ Backend Project.DeleteQuestion()
SubquestionIds auto-populated for system questions ✅ Backend Project.AnnotationQuestions getter

Rules Enforced ONLY by Frontend

Rule Enforcement Location
First-level custom questions in Treatment/DiseaseModel must parent to Control question ❌ Frontend Only CreateQuestionComponent.getCreateQuestion()
First-level custom questions in Cohort/Outcome/Experiment must parent to Label question ❌ Frontend Only CreateQuestionComponent.getCreateQuestion()
Nested custom questions parent to another custom question (same category) ❌ Frontend Only CreateQuestionComponent.getCreateQuestion()
Control parameter must be specified for Treatment/DiseaseModel categories (first-level only) ❌ Frontend Only CreateQuestionDialogData.control
Study category questions can be root (no system questions in Study) ❌ Frontend Only Implicit in dialog logic
Question text max length: 80 characters ❌ Frontend Only Form validators
Option values must be unique ❌ Frontend Only duplicateValidator

Lookup Questions

Lookup questions allow cross-referencing between annotation units.

How Lookups Work

  1. Label Question Creates Instance: When user answers a label question (e.g., "Treatment Name: Aspirin"), an annotation is created with ID = instance ID
  2. Lookup Question References Instances: Lookup questions display dropdown with all instances from referenced category
  3. Display Value vs. Stored Value:
  4. Display: Label annotation answer (e.g., "Aspirin")
  5. Stored: Label annotation ID (e.g., guid-of-aspirin-annotation)

Lookup Configuration

// From annotation-form.service.ts
switch (question.id) {
  case systemAnnotationQuestionGuids.cohortDiseaseModels:
    return getOptionsFor('diseaseModels');  // Returns Disease Model label annotations
  case systemAnnotationQuestionGuids.cohortTreatments:
    return getOptionsFor('treatments');      // Returns Treatment label annotations
  case systemAnnotationQuestionGuids.cohortOutcomes:
    return getOptionsFor('outcomes');        // Returns Outcome label annotations
  case systemAnnotationQuestionGuids.experimentCohorts:
    return getOptionsFor('cohorts');         // Returns Cohort label annotations
  case systemAnnotationQuestionGuids.outcomePdfGraphs:
    return getOptionsFor('pdfReferences');   // Returns PDF reference annotations
}

Empty Lookup Text

Each lookup question has an emptyLookupText property displayed when no instances exist:

  • "Empty - First Enter A Disease Model Induction Procedure"
  • "Empty - First Enter A Treatment Procedure"
  • "Empty - First Enter An Outcome Procedure"
  • "Empty - First Enter A Cohort"

Conditional Logic

Conditional Parent Answers

Questions can be conditionally displayed based on parent question answer.

Boolean Conditions

interface BooleanConditionalTargetParentOptions {
  conditionType: OptionType.Boolean;  // 0
  targetParentBoolean: boolean;       // true or false
}

Used for: Questions that appear only when parent checkbox is checked/unchecked

Option Conditions

interface OptionConditionalTargetParentOptions {
  conditionType: OptionType.Option;   // 1
  targetParentOptions: string[];      // Array of parent option values
}

Used for: Questions that appear only when specific parent options are selected

Schema Version Differences

Version Single Conditional Multiple Conditionals
V0 (Legacy) ✅ Allowed ❌ Not Allowed
V1+ ✅ Allowed ✅ Allowed

V0 Restriction: Target.V0SetConditionalParentAnswers() throws error if multiple options provided.

Option Parent Filters

Options themselves can have filters based on parent question answers:

interface ParentFilter {
  filterType: OptionType;  // Boolean or Option
  value: boolean | string[];
}

Example: Outcome Error Type options filtered by Average Type answer:

  • SD, SEM → only shown when Average = "Mean"
  • IQR → only shown when Average = "Median"

Frontend-Only Business Logic

Problem Statement

The following critical business logic exists ONLY in the frontend, specifically in:

  • CreateQuestionComponent.getCreateQuestion() (lines 346-402)
  • questionStateProjection selector
  • CreateQuestionDialogData interface

Logic That Should Be in Backend

// Current frontend logic (create-question.component.ts:369-402)
const target: Target | null = this.data.parentQuestion
  ? createTarget(...)  // Child of existing question
  : category === categories.cohort
    ? createTarget(systemAnnotationQuestionGuids.cohortLabel)  // ← HARDCODED RULE
    : category === categories.modelInduction
      ? createTarget(
          systemAnnotationQuestionGuids.modelControl,
          this.data.control  // ← CONTROL PARAMETER
        )
      : category === categories.outcomeAssessment
        ? createTarget(systemAnnotationQuestionGuids.outcomeAssessmentLabel)
        : category === categories.treatment
          ? createTarget(
              systemAnnotationQuestionGuids.treatmentControl,
              this.data.control  // ← CONTROL PARAMETER
            )
          : category === categories.experiment
            ? createTarget(systemAnnotationQuestionGuids.experimentLabel)
            : null;  // Study category - root question

Impact on Database Seeding

When seeding data programmatically (e.g., in ProjectSeeder.cs), these rules are not enforced, leading to:

  1. Invalid Question Structures: Custom questions created without proper parent references
  2. Missing Control Parameters: Treatment/DiseaseModel questions without control specification
  3. Incorrect Hierarchy: Questions that frontend would reject but backend accepts

Example of Invalid Seeded Data

// This would be INVALID according to frontend rules but backend accepts it:
new AnnotationQuestion {
    Category = "Treatment",
    Root = true,              // ❌ Should be false
    Target = null,            // ❌ Should point to Treatment Control
    // Missing control conditional
}

Recommendations

Short-Term Fixes

  1. Add Backend Validation: Implement category-specific parent validation in Project.UpsertCustomAnnotationQuestion()
  2. Fix Seeding Logic: Update seeding to follow frontend rules
  3. Add API Validation DTOs: Create validation attributes for UpsertCustomAnnotationQuestionDto

Long-Term Architecture Improvements

See Architecture Analysis Document for detailed recommendations on:

  • Moving domain logic to shared domain layer
  • Implementing rich domain model
  • Creating validation aggregates
  • API contract enforcement

Validation Rules to Implement in Backend

public class AnnotationQuestionValidator
{
    public ValidationResult Validate(UpsertCustomAnnotationQuestionDto dto, Project project)
    {
        // Rule 1: Treatment/DiseaseModel custom questions must parent to Control
        if (dto.Category == "Treatment" && dto.Target?.ParentId != TreatmentControlQuestionGuid)
            return ValidationResult.Error("Treatment questions must be children of Control question");

        if (dto.Category == "Disease Model Induction" && dto.Target?.ParentId != ModelControlQuestionGuid)
            return ValidationResult.Error("Disease Model questions must be children of Control question");

        // Rule 2: Cohort/Outcome/Experiment custom questions must parent to Label
        if (dto.Category == "Cohort" && dto.Target?.ParentId != CohortLabelQuestionGuid)
            return ValidationResult.Error("Cohort questions must be children of Label question");

        // ... etc
    }
}

Appendix: Code References

Frontend Files

File Purpose
annotation-question.entity.ts Core types, GUIDs, utility functions
create-question.component.ts Question creation logic (contains domain rules)
annotation-form.service.ts Form handling, lookup resolution
annotation-question.selectors.ts NgRx selectors for questions
config.service.ts Category UI configuration

Backend Files

File Purpose
AnnotationQuestion.cs Domain entity, system question factories
Target.cs Parent-child relationship
Project.cs Aggregate root, question management
OptionInfo.cs Option and filter types
Stage.cs Stage entity with Extraction boolean and AllStageAnnotationQuestions
Annotation.cs Annotation storage model with typed subclasses
ExtractionInfo.cs Container for annotations, sessions, and outcome data