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)
Related Documentation¶
- Formal Specification - Precise rule definitions, cross-language validation, referential integrity
- Architecture Analysis - Where this logic should live
- Category Question Structure - Detailed category rules
- Question Hierarchy Diagrams - Visual representations
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¶
- Core Concepts
- Stage Data Extraction Settings
- Question Visibility and Form Display
- Annotation Storage
- System Annotation Question GUIDs
- Category-Specific Rules
- Question Hierarchy Rules
- Lookup Questions
- Conditional Logic
- Frontend-Only Business Logic (Problem Area)
- 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:
- The annotation form expands to include all unit-based categories (Experiment, Cohort, Disease Model, Treatment, Outcome)
- Annotators can create structured units and link them together
- 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:
- Study - Always available, contains project-level questions
- Disease Model Induction - Define disease models (only when
extraction=true) - Treatment - Define treatments (only when
extraction=true) - Outcome Assessment - Define outcome measures (only when
extraction=true) - Cohort - Group disease models, treatments, and outcomes (only when
extraction=true) - 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:
- Form Display: Render the correct control type for each question
- Data Retrieval: Find all answers for a specific question across studies
- Export: Map annotation answers back to question text and category
- 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:
- Show Unit Categories - Make Experiment, Cohort, Disease Model, Treatment, and Outcome Assessment categories visible in the annotation form
- Enable Unit Creation - Allow annotators to create units (disease models, treatments, outcomes) with labels
- Enable Unit Linking - Allow Cohorts to link to Disease Models, Treatments, and Outcomes via lookup questions
- Enable Experiment→Cohort Linking - Allow Experiments to link to their Cohorts
- 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:
- Create Disease Models, Treatments, and Outcomes independently in their respective tabs
- Link them together via lookup questions in Cohort and Experiment tabs
- 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:
Averageis Mean ifaverageType = "Mean", Median ifaverageType = "Median"Erroris SD/SEM ifaverageType = "Mean", IQR ifaverageType = "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:
- An Experiment is created with a label
- A Cohort is created and linked to the Experiment
- 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¶
- API Endpoint:
ReviewController.SubmitSession - Service Layer: Converts DTO to domain objects
- Domain:
ExtractionInfo.AddOutcomeData()stores the data - Storage:
Study.ExtractionInfo.OutcomeDatacollection
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:
- Have
target.parentId=b18aa936-a4c6-446b-ac98-88ac38930878(modelControl) - Have
conditionalParentAnswersset to eithertrueorfalse(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:
- Have
target.parentId=d04ec2d7-3e10-4847-9999-befe7ee4c454(treatmentControl) - Have
conditionalParentAnswersset to eithertrueorfalse(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¶
- Root questions have
target: null - Child questions have
target.parentIdpointing to parent question ID - SubquestionIds on parent questions list child question IDs
- 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¶
- Label Question Creates Instance: When user answers a label question (e.g., "Treatment Name: Aspirin"), an annotation is created with ID = instance ID
- Lookup Question References Instances: Lookup questions display dropdown with all instances from referenced category
- Display Value vs. Stored Value:
- Display: Label annotation answer (e.g., "Aspirin")
- 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:
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)questionStateProjectionselectorCreateQuestionDialogDatainterface
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:
- Invalid Question Structures: Custom questions created without proper parent references
- Missing Control Parameters: Treatment/DiseaseModel questions without control specification
- 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¶
- Add Backend Validation: Implement category-specific parent validation in
Project.UpsertCustomAnnotationQuestion() - Fix Seeding Logic: Update seeding to follow frontend rules
- 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 |