QM v2 Migration Strategy — Zero-Disruption Rollout¶
Concrete migration steps from the current embedded question model to the QM v2 versioning architecture (DraftQuestion, AnnotationQuestion, PQS, SQS). Designed for zero downtime and per-project rollback. Covers data migration, system questions, annotation versioning integration, feature flag sequencing, and the annotation form transition.
Table of Contents¶
- Principles
- Migration Overview
- Pre-Migration: Additive Schema Changes (Zero Risk)
- Step 1: Create pmAnnotationQuestion Collection (Backend Only)
- Step 2: Per-Project Data Migration
- Step 3: Backfill Annotation Version References
- Step 4: Frontend Feature Flag Rollout
- Step 5: Annotation Form Transition
- Step 6: Legacy Decommission
- System Question Migration Detail
- Rollback Procedures
- Risk Matrix
- Environment Sequencing
1. Principles¶
| Principle | Why |
|---|---|
| Additive, never destructive | New fields/collections alongside old ones. Old data untouched until proven safe. |
| Per-project granularity | Each project migrates independently. One project's failure doesn't affect others. |
| Feature-flag gated | newQuestionManagement (admin UI) and annotationFormV2 (annotator UI) are independent flags. Each can be toggled without the other. |
| Dual-model period | Both old and new models coexist. Old code reads old data; new code reads new data. No shared writes. |
| Rollback at every step | Each step has a documented reversal that restores the previous state. |
| System questions are first-class | They get their own migration path because they have a fundamentally different lifecycle. |
2. Migration Overview¶
CURRENT STATE TARGET STATE
┌──────────────────────┐ ┌──────────────────────┐
│ pmProject │ │ pmProject │
│ ├ AnnotationQs[] │ ──────► │ ├ AnnotationQs[] │ (preserved, read-only)
│ │ (embedded, mutable)│ │ ├ DraftQuestions[] │ (NEW)
│ ├ Stages[] │ │ ├ ProjectQuestionSet │ (NEW)
│ │ └ AQs: Set<Guid> │ │ ├ DraftSnapshots[] │ (NEW)
│ └ SysQVersion: int │ │ ├ Stages[] │
└──────────────────────┘ │ │ └ StageQuestionSet │ (NEW)
│ └ SysQVersion: int │
┌──────────────────────┐ └──────────────────────┘
│ pmStudy │
│ └ Annotations[] │ ┌──────────────────────┐
│ └ QuestionId │ ──────► │ pmAnnotationQuestion │ (NEW collection)
└──────────────────────┘ │ ├ _id (= old AQ GUID)│
│ ├ publishedVersions[] │
│ └ draft: {...} │
└──────────────────────┘
┌──────────────────────┐
│ pmStudy │
│ └ Annotations[] │
│ ├ QuestionId │ (unchanged)
│ └ QuestionVersionId│ (NEW, nullable)
└──────────────────────┘
Key insight: The old AnnotationQuestions[] array on pmProject is never modified or deleted during migration. It becomes a read-only archive that unmigrated code paths continue to use. This is the cornerstone of zero-disruption rollback.
3. Pre-Migration: Additive Schema Changes (Zero Risk)¶
When: Deploy with normal CI/CD. No migration needed. No feature flag changes.
What: Ship the new domain entities and empty collection. The application code is deployed but the new code paths are dormant (behind feature flag = false).
| Change | Location | Impact on Running System |
|---|---|---|
Create pmAnnotationQuestion collection with projectId index |
MongoDB | None — empty collection, nothing reads it |
Add DraftQuestions: [] field default to Project entity |
C# model | None — deserialization ignores missing fields; new writes include empty array |
Add ProjectQuestionSet: null field default to Project entity |
C# model | None — null default |
Add StageQuestionSet: null field default to Stage entity |
C# model | None — null default |
Add DraftSnapshots: [] field default to Project entity |
C# model | None — empty array |
Add nullable QuestionVersionId to Annotation entity |
C# model | None — existing annotations deserialize with null |
Add /api/v2/ endpoints (behind feature flag check) |
PM service | None — endpoints return 403 when flag is off |
Backwards Compatibility: The MongoDB C# driver ignores unknown fields by default. Existing code reading pmProject or pmStudy documents will not see the new fields and won't break. New code writes default values alongside existing fields.
Rollback: Remove the code deployment. Empty collection auto-cleaned. No data was touched.
Deliverables:
- New domain entity classes with unit tests
- Repository implementations for pmAnnotationQuestion
- v2 API controllers (gated by feature flag)
- Collection creation + indexing in startup or migration script
4. Step 1: Create pmAnnotationQuestion Documents (Backend Only)¶
When: After Step 0 is deployed and stable. Triggered by a migration command, not by deployment.
What: For each project, create the new AnnotationQuestion documents in pmAnnotationQuestion and the initial PQS/SQS versions on the Project/Stage documents. This is a read-from-old, write-to-new operation.
4.1 Per-Project Migration Sequence¶
For each project P:
1. LOCK: Acquire advisory lock on P (prevent concurrent migration)
2. READ old state:
- P.AnnotationQuestions[] (embedded custom questions)
- P.SystemQuestionVersion (determines system question content)
- P.Stages[].AnnotationQuestions (HashSet<Guid> per stage)
3. CREATE system question AQ documents (see Section 10):
For each of the 17+1 system question GUIDs:
- Create AnnotationQuestion in pmAnnotationQuestion
_id = hardcoded GUID
projectId = P._id
system = true
publishedVersions = [AQVersion v1 with content from SystemQuestionCreators]
draft = null (system questions never have drafts)
4. CREATE custom question AQ documents:
For each custom question in P.AnnotationQuestions[]:
- Create AnnotationQuestion in pmAnnotationQuestion
_id = question.Id (GUID preserved)
projectId = P._id
system = false
publishedVersions = [AQVersion v1 with current question content]
draft = null (clean state — no pending changes)
category, parentQuestionId, dataType, groupAsSingle = frozen from current
scope = "project", ownerProjectId = P._id
5. CREATE initial PQS on Project:
- DraftPQS.orderedQuestions = all question IDs, type = "published"
(system questions first within each category, then custom, preserving
the order from the existing embedded array)
- PQSVersion v1:
orderedQuestionRefs = [{questionId, versionId: AQVersion._id}]
for ALL questions (system + custom)
6. CREATE initial SQS on each Stage:
For each stage S in P.Stages[]:
- DraftSQS.questionIds = S.AnnotationQuestions (the existing HashSet)
PLUS all system question GUIDs (if data extraction stage)
- SQSVersion v1:
pqsVersionId = PQSVersion v1._id
questionIds = same set
7. SET migration flag:
P.MigrationStatus = "migrated"
P.MigratedAt = DateTime.UtcNow
8. VALIDATE:
- Count of AQ docs == count of custom questions + 17/18 system questions
- Every stage's SQS questionIds is a subset of PQS questionIds
- Every annotation's QuestionId maps to an existing AQ doc
- PQS ordering is consistent (no duplicates, no orphans)
9. RELEASE lock
4.2 Fixing the 32 Known Production Violations¶
The characterization tests from the formal specification (Appendix A, 2025-12-27) found 32 custom questions in categories that require a parent (DMI, Treatment, Outcome, Cohort, Experiment) but have Target = null. These are in 24 projects, mostly from 2017–2019.
A production query (2026-03-25) checked which of these 32 questions have annotations against them. The result splits them into two groups:
23 Questions with Zero Annotations — Fix Before Migration¶
These questions have never been answered. Fixing their parent relationship is safe and has no downstream impact. During migration (or as a pre-migration cleanup step), set Target and parentQuestionId to the correct system question for their category:
- DMI questions → parent to DMI Control (
b18aa936-...) - Treatment questions → parent to Treatment Control (
d04ec2d7-...) - Outcome questions → parent to OA Label (
dbe2720c-...) - Experiment questions → parent to Experiment Label (
7c555b6e-...)
This brings them into alignment with the placement rules before they are frozen as AnnotationQuestion documents in the new model.
9 Questions with Annotations — Migrate via Legacy Experiment Instance Plan¶
These 9 questions are all in the Experiment category and therefore must be re-parented to Experiment Label in the new model. The migration cannot simply attach historical answers to arbitrary experiment instances, because their existing annotations were captured at study scope. To preserve data integrity without keeping an invalid hierarchy, the migration uses a deterministic per-study mapping plan:
| Project | Question | Category | Annotations |
|---|---|---|---|
| PC12 OGD Data Extraction | Statistical Test Used | Experiment | 22 |
| SR Tool Feature Analysis | Is HPLC used? | Experiment | 12 |
| Cannabinoids | What was the time (post model induction;hours) of outcome assessment measurement where the difference between control and treatment is greatest? | Experiment | 4 |
| Cannabinoids | What was the time (post model induction;hours) of the last outcome assessment measurement? | Experiment | 1 |
| ChABC SCI | Number of Controls | Experiment | 1 |
| ChABC SCI | Number in Treatment Group | Experiment | 1 |
| ChABC SCI | Total Number of Animals? | Experiment | 1 |
| ChABC SCI | Number of Sham Animals | Experiment | 1 |
| exposed to A have effects on B | is this a trial or observation study | Experiment | 1 |
Migration algorithm:
- Dry-run inventory every affected project and study:
- impacted question IDs, annotation counts, and answer payload counts
- existing Experiment units per study
- whether each study has zero, one, or multiple candidate Experiment instances
- Re-parent the question definition to
Experiment Labelbefore the AQ document is frozen, so the resultingAnnotationQuestioncomplies with the system-question hierarchy. - Map existing answers per study using a no-guess rule:
- If the study has exactly one existing Experiment instance, attach the migrated answers to that instance.
- If the study has zero or multiple Experiment instances, create a synthetic legacy Experiment instance for migration and attach all impacted answers to that instance.
- Create audit metadata for every migrated answer and synthetic instance:
- source question ID
- source study ID
- migration run ID / timestamp
- mapping mode (
existing-instanceorsynthetic-legacy-instance) - Validate before commit:
- answer count preserved exactly
- no answer duplicated or dropped
- every migrated answer references a valid Experiment instance and a valid AQVersion
- no affected question remains parentless after migration
- Validate after commit:
- project-level reconciliation report matches pre-migration counts
- affected stages render the question under Experiment Label
- quantitative export / annotation reconstruction smoke tests pass for the 5 affected projects
This preserves every historical answer exactly once while bringing the question definitions into compliance with the system-question model. The synthetic legacy instance is intentionally explicit: it preserves historical scope without pretending the system can infer which experiment instance an old root-level answer belonged to.
Strategy Summary¶
| Group | Count | Action | Rationale |
|---|---|---|---|
| Zero annotations | 23 | Fix parent before migration | No data impact; brings questions into compliance before freezing |
| Has annotations | 9 | Re-parent to Experiment Label and migrate answers into existing or synthetic Experiment instances | Preserves answers while enforcing the canonical system-question hierarchy |
| All future questions | — | Prevent new violations | New validation blocks creating parentless questions in categories that require one |
4.3 Dry-Run Mode¶
The migration service supports dryRun: true which:
- Reads all data and performs validation
- Outputs what WOULD be created (AQ count, PQS structure, SQS assignments)
- Writes nothing
- Reports any anomalies (missing questions, orphan references, placement violations)
Rollback: Delete all pmAnnotationQuestion documents with projectId = P._id. Remove ProjectQuestionSet, DraftQuestions, and StageQuestionSet fields from the Project document. The existing AnnotationQuestions[] array is untouched and continues to work.
5. Step 2: Backfill Annotation Version References¶
When: After Step 1 for a project. Can run immediately after or as a separate pass.
What: Add QuestionVersionId to every existing annotation on pmStudy, pointing to the corresponding AQVersion v1.
// Pseudocode — actual implementation in C# via MongoDb driver
for each study S where S.ProjectId == P._id:
for each annotation A in S.Annotations[]:
AQ = pmAnnotationQuestion.findOne({_id: A.QuestionId, projectId: P._id})
if AQ exists:
A.QuestionVersionId = AQ.publishedVersions[0]._id // v1
else:
log.warn("Annotation references unknown question", A.QuestionId)
A.QuestionVersionId = null // tolerate orphan
Why separate from Step 1: Study documents can be large and numerous. Batching this separately avoids long-running transactions that block the migration lock.
Performance: Process in batches of 100 studies. Use bulkWrite with updateOne operations. Expected throughput: ~1000 studies/minute on Atlas M20.
Rollback: $unset the QuestionVersionId field from all annotations:
db.pmStudy.updateMany(
{ ProjectId: CSUUID("project-id") },
{ $unset: { "Annotations.$[].QuestionVersionId": "" } }
)
6. Step 3: Frontend Feature Flag Rollout¶
When: After Steps 1–2 for target projects. The backend is ready; this step enables the new admin UI.
6.1 Staged Rollout¶
| Stage | Scope | Duration | Gate |
|---|---|---|---|
| Internal | SyRF team test projects only | 1 week | Team validates all views work, publish flow succeeds |
| Small projects | Projects with < 20 questions, < 5 stages | 1 week | No user-reported issues |
| Medium projects | Projects with < 100 questions | 1 week | Data export still produces identical output |
| All projects | Enable for all migrated projects | Ongoing | Monitor for 2 weeks |
6.2 Feature Flag Architecture¶
newQuestionManagement:
scope: per-project (not global)
default: false
controls:
- Sidebar nav shows Questions > Design/Assign/Preview
- Admin routes to v2 components
- API calls go to /api/v2/ endpoints
- Question loading via SQS → PQS → pmAnnotationQuestion chain
annotationFormV2:
scope: global (affects all users, not just admins)
default: false
controls:
- Unit mini cards with select-to-show workspace
- Signal forms replacing reactive forms
- Per-unit form state
NOTE: Independent of newQuestionManagement.
Can be enabled for unmigrated projects too
(renders existing questions with new UI).
6.3 What Users See¶
Admins (when newQuestionManagement = true for their project):
- Sidebar nav gains "Questions" section with Design, Assign, Preview
- Old QM interface no longer accessible for this project
- All question editing, assignment, publishing happens through v2 UI
- Autosave replaces manual save
Annotators (when annotationFormV2 = false, which is the default):
- No visible change. The annotation form loads questions through the new backend chain (SQS → PQS → AQ) but renders them identically to before.
- Annotations now record QuestionVersionId alongside QuestionId — transparent to the annotator.
Annotators (when annotationFormV2 = true, later rollout):
- Unit tiles, mini cards, multi-expand workspace
- Same questions, same conditional logic, new presentation
Rollback: Set newQuestionManagement = false for the project. Admin UI reverts to legacy QM. Backend continues serving old data from embedded AnnotationQuestions[]. The new data in pmAnnotationQuestion sits dormant.
7. Step 4: Annotation Form Transition¶
When: After Step 3 is stable for all projects. This is the second, independent feature flag rollout.
7.1 Two Layers of Change¶
The annotation form has two independent concerns:
| Concern | Flag | Who It Affects | Risk Level |
|---|---|---|---|
| Question resolution backend | newQuestionManagement |
Annotators (indirectly — same questions, new loading path) | Low — output is identical |
| UI: unit tiles, signal forms | annotationFormV2 |
Annotators (directly — new interaction patterns) | Medium — UX change |
These are deployed independently:
- First: Enable
newQuestionManagement→ annotators unknowingly use the new backend. Monitor that question rendering is identical. - Later: Enable
annotationFormV2→ annotators see the new UI. This can be rolled back independently.
7.2 Annotation Form Dual-Model Support¶
During the transition, the annotation form component must handle both:
// Simplified resolution logic
if (project.migrationStatus === 'migrated') {
// New path: SQS → PQS → pmAnnotationQuestion
const sqsVersion = stage.stageQuestionSet.latestVersion;
const pqsVersion = project.projectQuestionSet.getVersion(sqsVersion.pqsVersionId);
const questions = resolveQuestions(pqsVersion, sqsVersion);
// Each annotation records QuestionVersionId on save
} else {
// Legacy path: embedded questions from Project
const questions = project.annotationQuestions.filter(q => stage.annotationQuestions.has(q.id));
// QuestionVersionId is null on save (unmigrated)
}
7.3 Data Export Compatibility¶
Critical invariant: Data export must produce byte-identical output regardless of which model a project uses.
| Export Concern | Why It's Safe |
|---|---|
OutcomeData population |
Uses QuestionId (hardcoded system GUID), not QuestionVersionId. Unchanged. |
OutcomeDataFormatRowWriter |
Queries annotations by QuestionId. The GUID is preserved through migration. |
| Custom question export | Queries annotations by QuestionId. The GUID is preserved. Version info is additional metadata, not a breaking change. |
| Annotation grouping | Uses QuestionId + AnnotatorId + StudyId as composite key. Unchanged. |
Validation: Run data export for migrated projects and diff against pre-migration export. Must be identical.
8. Step 5: Legacy Decommission¶
When: After all projects migrated, all feature flags enabled, stable period of 4+ weeks.
What: Remove the dual-model support code and the old embedded data.
| Action | When | Reversible? |
|---|---|---|
| Remove feature flag checks from frontend | After 100% rollout + 4 weeks stable | Yes (re-add checks) |
| Remove legacy QM v1 components | After feature flag removal | Yes (git revert) |
| Remove old API endpoints | After frontend no longer calls them | Yes (git revert) |
Archive AnnotationQuestions[] from pmProject |
After 8+ weeks stable | Yes (restore from archive) |
| Drop dual-model support from annotation form | After annotationFormV2 is 100% |
Yes (git revert) |
The embedded AnnotationQuestions[] array is the LAST thing to go. It serves as the rollback source for the entire migration. Only remove it after extended production stability.
9. System Question Migration Detail¶
System questions require special handling because they are dynamically generated in the current system (not persisted) but must become persisted versioned entities in the new system.
9.1 Migration Creates, Not Updates¶
For system questions, Step 1 is a CREATE operation, not an update:
Current: System questions exist only as C# constants + factory methods
No document in any MongoDB collection
After: AnnotationQuestion document in pmAnnotationQuestion
_id = hardcoded GUID (e.g., bdb6e257-5a08-42ef-aad0-829668679b0e)
system = true
publishedVersions = [AQVersion v1 with content from SystemQuestionCreators]
9.2 SystemQuestionVersion-Dependent Content¶
The CreateOutcomeErrorTypeQuestion factory method generates different content based on Project.SystemQuestionVersion. The migration must:
- Read
Project.SystemQuestionVersion - Call
SystemQuestionCreators[errorTypeGuid](project)to get the version-correct content - Persist that content as AQVersion v1
This means two projects with different SystemQuestionVersion values will have different AQVersion v1 content for the Error Type question. That's correct — each project's system questions reflect its configured version.
9.3 Post-Migration: Explicit System-Question Upgrade Handling¶
After migration, if a platform update bumps the system question definitions:
- A system admin runs an explicit upgrade command or admin workflow after the new
SystemQuestionVersionships - The upgrade service performs a dry-run diff showing:
- affected system questions
- projects currently on the older version
- whether each project can be upgraded immediately or deferred
- The admin chooses all projects or a subset of projects
- For the selected projects, the admin chooses an execution mode:
- Immediate upgrade: create new AQVersions for affected system questions and append a new PQS version now
- Mark pending for lazy upgrade: record the project as pending and create the new AQVersions/PQS version the next time QM loads that project
- Stages show "system questions updated — publish to apply" until the stage is republished onto the newer PQS version
- The upgrade run writes execution metadata for audit, retry, and rollback analysis
This makes the version bump an intentional operational event, while still allowing lazy per-project completion when the system admin decides that is the safer rollout mode.
9.4 System Questions in the Assign View¶
System questions are auto-included in data extraction stages and cannot be unassigned:
- Checkbox: checked + disabled
- Badge: "System" with lock icon
- Not counted in "pending changes" totals
- Not part of admin publish actions (they version separately)
9.5 System Questions and OutcomeData¶
No change to data export. The OutcomeDataFormatRowWriter identifies system question annotations by QuestionId (the hardcoded GUID). The GUID is the _id of the new AnnotationQuestion document — same value, different storage location. Export code never needs to know about QuestionVersionId.
10. Rollback Procedures¶
Every step has independent rollback. Rollback order is reverse of migration order.
Per-Project Rollback (Reverses Steps 1–3)¶
1. Set feature flag: newQuestionManagement = false for project P
→ Admin UI immediately reverts to legacy QM
→ Annotators unaffected (annotation form falls back to legacy path)
2. Remove QuestionVersionId from annotations:
db.pmStudy.updateMany(
{ ProjectId: CSUUID("P._id") },
[{ $set: { "Annotations": {
$map: { input: "$Annotations", as: "a", in: {
$mergeObjects: ["$$a", { QuestionVersionId: null }]
}}
}}}]
)
3. Clear new fields from Project document:
db.pmProject.updateOne(
{ _id: CSUUID("P._id") },
{ $unset: {
"ProjectQuestionSet": "",
"DraftQuestions": "",
"DraftSnapshots": "",
"MigrationStatus": "",
"MigratedAt": ""
}}
)
4. Clear StageQuestionSet from each Stage:
db.pmProject.updateOne(
{ _id: CSUUID("P._id") },
{ $unset: { "Stages.$[].StageQuestionSet": "" } }
)
5. Delete AQ documents for this project:
db.pmAnnotationQuestion.deleteMany({ projectId: CSUUID("P._id") })
6. Verify: Existing AnnotationQuestions[] on Project is intact
→ This was NEVER modified during migration
Global Rollback (Reverses Step 0)¶
Only needed if the schema changes themselves cause problems (extremely unlikely):
- Set all feature flags to false
- Deploy previous application version (git revert)
- Drop
pmAnnotationQuestioncollection - The application reads existing embedded data as before
11. Risk Matrix¶
| Risk | L | I | Mitigation | Detection |
|---|---|---|---|---|
| Migration creates incorrect AQVersion content | Low | High | Dry-run mode validates all content before writing. Diff old vs new. | Post-migration validation queries |
| System question GUID collision with custom question | Impossible | Critical | System GUIDs are hardcoded constants that never overlap with random GUIDs | Migration validates uniqueness |
| Concurrent annotation during migration | Medium | Medium | Migration runs per-project with advisory lock. Annotations save to Study (different document). Lock duration: < 30 seconds per project. | Lock timeout alerts |
| pmProject document exceeds 16MB after adding PQS/SQS | Low | High | PQS and SQS are compact (ordered GUIDs). Even 2000 questions = ~80KB of refs. DraftSnapshots capped at 50 with deduplication. | Document size monitoring post-migration |
| Data export produces different output | Low | Critical | Export uses QuestionId (preserved GUID), not QuestionVersionId. Integration test: export before and after migration, diff output. | CI test with production data snapshot |
| Feature flag rollback causes state inconsistency | Low | Medium | Old code reads old data (AnnotationQuestions[]); new data in pmAnnotationQuestion is dormant. No shared writes during dual-model period. | Smoke tests after flag toggle |
| SystemQuestionVersion bump during migration window | Low | Medium | Freeze the target SystemQuestionVersion per migration run. If the platform ships a newer version mid-run, complete the current run and schedule a separate explicit upgrade run for affected projects. |
Version check in migration validation + upgrade run audit trail |
| Annotated Experiment migration maps answers to the wrong unit | Low | High | No-guess rule: reuse an existing Experiment instance only when exactly one exists; otherwise create a synthetic legacy instance. Validate counts before/after and retain mapping audit metadata. | Dry-run classification report + per-project reconciliation checks |
12. Environment Sequencing¶
Timeline¶
Week 1: Step 0 — Deploy schema changes to staging
Verify: no regressions, empty collection exists, new fields default correctly
Run: full test suite, data export comparison
Week 2: Pre-migration cleanup — Repair all 32 placement violations
Run: dry-run to classify 23 zero-annotation fixes and the 9 annotated Experiment migrations
Fix: set Target + parentQuestionId on 23 questions; stage legacy Experiment instance mappings for the 9 annotated questions
Verify: per-project reconciliation reports, affected-stage rendering, and export smoke tests for the 5 impacted projects
Step 1 — Migrate staging projects
Run: dry-run first, then actual migration
Verify: AQ documents created, PQS/SQS populated, validation passes
Step 2 — Backfill annotation version refs on staging
Verify: all annotations have QuestionVersionId
Week 3: Step 3 — Enable newQuestionManagement flag on staging
Internal testing: Design view, Assign view, Preview, publish flow
Verify: publish creates new AQVersions, PQS/SQS versions
Run: data export comparison (must be byte-identical)
Week 4: Step 0 — Deploy schema changes to production
Step 1 — Migrate production projects (batch: 10 at a time, smallest first)
Step 2 — Backfill production annotations
Verify per-batch: validation passes, data export unchanged
Week 5: Step 3 — Enable newQuestionManagement on production
Staged rollout: internal → small → medium → all (see §6.1)
Monitor: error rates, export correctness, admin feedback
Week 6+: Step 4 — Enable annotationFormV2 (separate rollout)
Staged: internal → small sample → all
Monitor: annotator feedback, session completion rates
Week 10+: Step 5 — Legacy decommission (after 4+ weeks stable)
Monitoring Checklist (Per Environment)¶
| Metric | Threshold | Action |
|---|---|---|
| pmAnnotationQuestion document count | = expected per project | Alert if mismatch |
| Data export diff (migrated vs unmigrated) | 0 differences | Block further rollout |
| API error rate on /api/v2/ endpoints | < 0.1% | Investigate, consider rollback |
| Annotation save latency (p95) | < 500ms (same as baseline) | Investigate if degraded |
| pmProject document size (max) | < 4MB | Alert if approaching |
| Migration duration per project | < 60 seconds | Investigate outliers |
| Feature flag toggle latency | < 1 second | N/A (config change) |
Appendix A: Relationship to PR2398 Annotation Versioning¶
The QM v2 migration (this document) establishes the question versioning foundation. The full annotation versioning model from PR2398 is a subsequent workstream that builds on top:
| QM v2 Migration (This Document) | PR2398 Annotation Versioning (Future) |
|---|---|
AnnotationQuestion with publishedVersions[] |
Annotation with annotationVersions[] (AV) |
PQSVersion / SQSVersion |
SessionVersion (ASV) pinning AV refs |
QuestionVersionId on existing Annotation |
Full AnnotationVersion entity with pendingAnswer, committedBy, createdByAction |
Annotations remain embedded on pmStudy |
Annotations extracted to pmAnnotation collection |
Sessions remain embedded on pmStudy |
Sessions extracted to pmAnnotationSession collection |
| Admin publishes stages (new AQVersions) | Admin Decision Framework evaluates session impact |
| No session pinning (latest SQS always used) | ASV reconstruction algorithm, PAC evaluation |
Sequencing: QM v2 migration MUST complete before PR2398 annotation versioning begins. The QuestionVersionId backfill (Step 2) creates the foreign key that PR2398's AnnotationVersion model will reference.
Publish Decision Initialization During Migration¶
When the migration creates AQVersion v1 for each question (Step 1), the publish decision fields are initialized as follows:
| Field | Initial Value | Rationale |
|---|---|---|
breakingChange |
false |
Migration preserves existing semantics — no answer validity change |
publishDecisions |
[] (empty array) |
No admin decisions needed — v1 is the baseline, not a change |
audit.action |
"migration" |
Distinguishes migration-created versions from admin-created ones |
audit.triggeredBy |
migration run ID | Links to migration execution metadata for traceability |
No DraftPublishDecision is created during migration because there are no pending changes — the draft field on each AQ is null (clean state). The publish decision workflow only activates post-migration when an admin edits a published question that has annotations, at which point the properties panel's Impact & Mapping section auto-expands (see mental-model-review-decisions.md Section 9).
Annotation and Session Extraction — Explicit Deferral¶
QM v2 migration keeps annotations and sessions embedded on pmStudy. Extraction to dedicated collections (pmAnnotation, pmAnnotationSession) is explicitly deferred to a future phase because:
- Not required for question versioning — the
QuestionVersionIdbackfill (Step 2) is sufficient to link annotations to AQ versions while they remain embedded - Avoids compounding migration risk — extracting annotations is a high-volume data transformation that should not be bundled with the question model migration
- Independent rollback — keeping extraction separate means the QM v2 migration can be rolled back without also unwinding annotation extraction
What this means for Phases 1–7:
- Annotations remain on pmStudy.Annotations[] with the new QuestionVersionId field
- Concurrent annotators still contend on the Study document (existing behaviour, unchanged)
- Per-annotation pendingAnswer auto-save (from PR2398) is NOT available until extraction
- Full annotation version history (AnnotationVersion entity) is NOT available until extraction
Planned future phase (after QM v2 Phases 1–9 are stable):
- Extract annotations from pmStudy to pmAnnotation collection
- Extract sessions from pmStudy to pmAnnotationSession collection
- Enable per-annotation pendingAnswer, full AV history, and ASV-based session pinning
- See annotation-versioning-integration.md Section 5.5 for the extraction scope
Appendix B: Decision Log¶
| # | Decision | Rationale |
|---|---|---|
| D1 | Per-project migration, not global batch | Limits blast radius. Allows staged rollout. Individual project rollback. |
| D2 | Preserve embedded AnnotationQuestions[] throughout migration |
Serves as rollback source. Old code continues to read it. Only removed in final decommission. |
| D3 | System questions jump straight to AnnotationQuestion (skip DraftQuestion) |
System questions are never in draft state. They are system-defined and published from birth. |
| D4 | SystemQuestionVersion bumps are handled by an explicit system-admin upgrade workflow, with per-project immediate or lazy execution modes | Makes system-question changes an intentional operational event, supports dry-run review, and still allows lazy per-project completion when desired. |
| D5 | Two independent feature flags (newQuestionManagement, annotationFormV2) |
Admin UI changes and annotator UI changes have different risk profiles and different user populations. Independent rollback. |
| D6 | Backfill QuestionVersionId as a separate step from AQ document creation |
Study documents can be large and numerous. Separate batching avoids long locks. |
| D7 | Repair all 32 placement violations during migration | Production query (2026-03-25) confirmed 23 of 32 violated questions have zero annotations and can be fixed directly. The remaining 9 annotated Experiment questions are re-parented to Experiment Label and their answers are migrated into deterministic existing or synthetic legacy Experiment instances so the final model is consistent and no data is lost. |
| D8 | Data export uses QuestionId (not QuestionVersionId) |
Hardcoded GUIDs are the stable identity. Version refs are additional metadata. Export code unchanged. |