Annotation Versioning Integration with QM v2¶
Reconciliation of the PR2398 annotation versioning spec with the PR2461 QM v2 implementation plan. Identifies divergences, resolves them, and provides a unified reference for how annotation versions, sessions, and the annotation form interact with the new question management features.
Source Documents¶
| Document | Worktree | Authority |
|---|---|---|
| annotation-versioning/README.md | PR2398 | Authoritative for entity model (AQ, AQV, QS, QSV, Annotation, AV, Session, ASV) |
| annotation-versioning/design-session.md | PR2398 | Authoritative for decisions D37-D57, DraftAQ lifecycle, pending buffers |
| question-management/README.md | PR2398 | Authoritative for QM workflows, API design, admin decision framework |
| question-management/implementation-plan.md | PR2461 | Current implementation plan with 9-phase breakdown |
| question-management/publishing-versioning-ux.md | PR2461 | UI-focused entity model and publishing ceremony |
| question-management/ui-specification.md | PR2461 | Design/Assign/Preview view specifications |
| Memory: project_qm_design_decisions.md | Memory | Design session decisions (2026-03-21 to 2026-03-24) |
| question-management/mental-model-review-decisions.md | PR2461 | Mental model audit decisions (2026-03-28 to 2026-03-30): PublishDecision entity, VersionAudit, Phase 8 sub-phases, consistency invariant, concurrency, session handling |
Fundamental Consistency Invariant¶
An annotation can only exist in a session version if its answer is valid for the AQ version referenced by that session's SQS version. No publish can create a session version containing an invalid annotation.
This invariant is the single most important correctness guarantee in the versioning system. It constrains the publishing flow (which must validate all annotations before creating session versions), the session transition engine (which must resolve or reject invalid answers), and the conflict resolution wizard (which surfaces cases where per-question decisions would violate this invariant). Every entity and process described in this document must uphold it.
1. Key Divergences Between PR2398 and PR2461¶
1.1 Entity Identity: Composite vs GUID¶
| Aspect | PR2398 (D57) | PR2461 Implementation Plan |
|---|---|---|
| AQVersion identity | Composite: (QuestionId, VersionNumber) |
GUID _id on each AQVersion |
| PQS/SQS version identity | Composite: (QuestionSetId, VersionNumber) |
GUID _id on each version |
| Cross-aggregate references | Composite Value Objects (AQVersionRef, QSVersionRef) |
GUID references (versionId, pqsVersionId) |
Resolution: PR2461's GUID approach is pragmatic for MongoDB (BinData fields are native, composite lookups require either application-level joins or compound indexes). However, PR2398's DDD reasoning is sound. Recommendation: Keep GUID _id on version documents for MongoDB efficiency, but also store versionNumber: int for human-readable ordering and debugging. Cross-aggregate references use the GUID (e.g., Annotation.QuestionVersionId points to AQVersion._id). This is what the PR2461 implementation plan already specifies.
1.2 Question Set Architecture: QS vs PQS/SQS¶
| Aspect | PR2398 | PR2461 |
|---|---|---|
| Question set entity | Single QuestionSet (QS) per stage with QSVs |
ProjectQuestionSet (PQS, project-wide) + StageQuestionSet (SQS, per-stage subset) |
| Ordering | In QSV: AQVersionIds: OrderedList<Guid> |
In PQS: ordered array; SQS: unordered Set<Guid> (order derived from PQS) |
| Cross-stage sharing | Implicit via parent integrity creating overlap | Explicit via PQS being the superset that all SQSes reference |
| Collection | pmQuestionSet |
PQS embedded on pmProject, SQS embedded on Stage |
Resolution: The PQS/SQS model in PR2461 is a superset of PR2398's QS model. PR2398's QSV becomes the SQS version. The PQS is an additional layer that: - Provides a single source of truth for question ordering across all stages - Makes cross-stage question overlap explicit rather than derived - Simplifies the assign view (SQS is a subset of PQS, not an independent composition)
The PR2398 spec's QSV rules (parent integrity, immutability, version history) apply directly to PQS versions. SQS versions are simpler because they only track which questions are included (no ordering, no version refs — both derived from the referenced PQS version).
1.3 AQ Collection Location¶
| Aspect | PR2398 | PR2461 |
|---|---|---|
| Where AQs live | pmAnnotationQuestion collection (separate from Project) |
pmAnnotationQuestion collection (separate) for published; DraftQuestions[] embedded in pmProject for drafts |
| Scope/OwnerId fields | Yes (System/Organisation/Researcher/Project) | Yes (scope, ownerProjectId) — but deferred to future |
Resolution: Aligned. Both specs agree on pmAnnotationQuestion as a separate collection. PR2461 adds the DraftQuestion concept embedded on Project (equivalent to PR2398's DraftAQ on Project aggregate). The PR2461 model correctly carries the GUID forward from DraftQuestion to AnnotationQuestion on first publish.
1.4 Annotation and Session Collections¶
| Aspect | PR2398 | PR2461 |
|---|---|---|
| Annotation storage | pmAnnotation — NEW separate collection |
Existing pmStudy.Annotations[] with added QuestionVersionId |
| Session storage | pmAnnotationSession — NEW separate collection |
Not specified in implementation plan (implied: existing embedded approach) |
| Study back-references | None (D50-revised) | Not addressed |
Resolution — CRITICAL DIVERGENCE: PR2398 specifies extracting annotations into pmAnnotation and sessions into pmAnnotationSession (decisions D41, D42, D50-revised). PR2461 does NOT plan this extraction — it only adds QuestionVersionId to the existing embedded Annotations[] on pmStudy.
This is the most significant gap between the two specs. The implications:
- Without extraction: Concurrent auto-save from multiple annotators still contends on the Study document. The
pendingAnswerconcept from PR2398 cannot be implemented per-annotation without per-annotation documents. - With extraction: Major migration effort, but enables the full PR2398 auto-save and versioning model.
- Pragmatic middle ground: Keep annotations embedded on Study for Phase 1, but implement optimistic concurrency (already in PR2461 Phase 1). Plan extraction as a future phase when the annotation form v2 needs per-annotation auto-save.
Recommendation: The PR2461 implementation plan should explicitly defer annotation/session extraction to a later phase but design the data model now so that the migration path is clear. Specifically:
- Add QuestionVersionId: Guid? to existing Annotation on pmStudy (Phase 1 — already planned)
- Design domain entities for Annotation and AnnotationSession as if they were separate aggregates
- Keep them embedded on Study for now with optimistic concurrency
- Plan the extraction migration as a dedicated phase after the QM v2 rollout
1.5 Annotation Version (AV) Model¶
| Aspect | PR2398 | PR2461 |
|---|---|---|
| AV entity | Full entity with versionNumber, value, notes, aqVersionRef, qsvRef, stageId, committedBy, createdByAction, sessionVersionRef |
Not modelled — only QuestionVersionId: Guid? added to existing Annotation |
| pendingAnswer | Mutable field on Annotation, server-side auto-save | Not specified |
| Version history | Append-only avs[] on Annotation |
No annotation versioning planned |
Resolution: PR2461 focuses on question versioning, not annotation versioning. The annotation versioning model from PR2398 should be implemented in Phase 8 (Admin Decision Framework) or as a separate subsequent phase. For now, the QuestionVersionId on annotations is sufficient to link answers to question versions.
What PR2461 needs immediately: When the annotation form loads questions for a session, it must resolve which AQVersion to show. The current plan uses the SQS version's pqsVersionId to find the PQS version, which contains {questionId, versionId} pairs. This is correct and sufficient without full annotation versioning.
1.6 Session Versioning¶
| Aspect | PR2398 | PR2461 |
|---|---|---|
| ASV (Session Version) | Full entity with annotationAVMap, qsvRef, resolvedAQVersionRefs, status |
Not modelled |
| Session pinning | Each ASV explicitly pins AV versions | Not planned |
| QSV transition | ASV reconstruction algorithm (lines 242-262 of QM README) | Phase 8 — admin decision framework skeleton |
Resolution: Session versioning is not needed for Phase 1-7 of the QM v2 rollout. The existing AnnotationSession with status: Incomplete|Complete continues to work. Session versioning becomes necessary when:
1. Admin wants to change stage question sets while annotations are in progress (Phase 8)
2. Cross-stage PAC consistency needs to be tracked (future)
3. Full audit trail from question to answer is required (production hardening)
Recommendation: Phase 8 of the PR2461 plan already covers the Admin Decision Framework. It should explicitly reference the ASV reconstruction algorithm from PR2398 as the authoritative specification. The PR2461 implementation plan already notes this at line 47: "the ASV reconstruction algorithm (PR2398 README lines 242-262) is the authoritative spec."
2. Annotation Form Integration¶
2.1 How the Annotation Form Resolves Questions¶
Current (legacy): The annotation form loads Stage.AnnotationQuestions (a HashSet<Guid>) and fetches the full embedded question objects from the Project.
With QM v2 (migrated project):
1. Load SQS for the stage
→ Get latest SQSVersion (or pinned version if session versioning is active)
→ SQSVersion contains: pqsVersionId + Set<Guid> questionIds
2. Load PQS version referenced by pqsVersionId
→ PQSVersion contains: orderedQuestionRefs [{questionId, versionId}]
3. Filter PQS refs to only those in the SQS questionIds set
→ Result: ordered list of {questionId, versionId} for this stage
4. Load AQ documents from pmAnnotationQuestion for those questionIds
→ Each AQ has publishedVersions[] — find the version matching versionId
5. Render questions using the resolved AQVersion content
→ Text, options, helpText, answerFilters from the pinned version
2.2 What Changes in the Annotation Form Component¶
| Concern | Current Behaviour | QM v2 Behaviour | Phase |
|---|---|---|---|
| Question loading | Embedded on Project | Resolved via SQS → PQS → pmAnnotationQuestion | Phase 3 (API) |
| Question ordering | Derived from embedded array | Derived from PQS version filtered by SQS | Phase 3 |
| Version awareness | None — single question definition | Each annotation records which AQVersion it was answered against | Phase 3 |
| Conditional logic (PAC) | Frontend-only via Target / answerOptionFilters |
Same logic, but using versioned answerFilters from AQVersion |
Phase 3 |
| Auto-save | Saves to Study document | Saves to Study document (unchanged until annotation extraction) | Unchanged |
| Unit tiles / collapsing | Not implemented | New mini card UI with select-to-show workspace | Phase 7 |
| Per-question form state | One giant form for all questions | Per-unit form state via signal forms | Phase 7 |
2.3 Dual-Model Support Period¶
During the transition, the annotation form must support both:
- Unmigrated projects: Load questions from embedded Project.AnnotationQuestions, no version awareness
- Migrated projects: Load questions via SQS → PQS → pmAnnotationQuestion chain, record QuestionVersionId on annotations
The newQuestionManagement feature flag controls this. A separate annotationFormV2 flag (PR2461 implementation plan line 702) controls the unit tile UI changes independently.
3. Publishing Flow and Its Effect on Annotation Sessions¶
3.1 What Happens When Admin Publishes a Stage¶
The PR2461 publishing ceremony (per publishing-versioning-ux.md):
Admin clicks "Publish Stage" on Assign view
│
├─ Validate: cross-question coherence for this stage's questions
│
├─ For each question with draft changes that is assigned to this stage:
│ Create new AQVersion from AQ.draft content
│ Clear AQ.draft
│
├─ Create new PQSVersion
│ orderedQuestionRefs: [{questionId, versionId}] for ALL project questions
│ (versionId = newly created AQVersion for changed questions,
│ existing latest version for unchanged questions)
│
├─ Create new SQSVersion for THIS stage
│ pqsVersionId: new PQSVersion._id
│ questionIds: Set<Guid> of questions assigned to this stage
│
└─ Other stages NOT auto-updated
Show "updated version available" indicator on other stages' Assign views
3.1.1 Concurrency Re-Validation at Publish Time¶
Annotations may be submitted between the admin opening the publish wizard and clicking Publish, invalidating the impact assessment. The system uses optimistic concurrency with re-validation:
- Admin opens publish wizard — system computes annotation impact and stores a snapshot
- Admin makes decisions, clicks Publish
- System acquires a brief lock on relevant stage/studies
- System re-computes annotation impact
- Compares against snapshot from step 1:
- If unchanged: proceed with publish
- If changed: abort, refresh dialog with updated counts. Admin's previous decisions are preserved. Questions that were auto-confirmed (zero annotations) but now have annotations lose auto-confirm status
- Publish executes atomically within lock — AQVersions, PQSVersion, SQSVersion created, session transitions executed, lock released
- Lock duration is brief (sub-second for write operations)
This ensures the consistency invariant is maintained even under concurrent annotator activity.
3.2 Impact on Active Annotation Sessions¶
If no session versioning (Phases 1-7): Active annotation sessions always see the latest published SQS version. When an admin publishes: - New sessions see the new SQS version - Existing incomplete sessions see the new questions on next load - No explicit pinning — sessions implicitly use "latest"
With session versioning (Phase 8): The PR2398 Admin Decision Framework activates, now refined into a 5-step publish wizard (see Section 5.4):
- Review changes: Admin sees what changed since last publish for this stage
- Confirm impact: Per-question classification — "does not affect existing answers" / "may affect existing answers"
- Configure handling: Per-question decisions with answer distribution shown, separate sections for in-progress and completed sessions
- Resolve conflicts: Multi-question conflict resolution (see below)
- Review & publish: Session impact summary, final confirmation
The ASV reconstruction algorithm evaluates PAC top-down through the question tree, classifying each question as carried-forward, migrated, breaking, new, newly-visible, or removed.
Transition ASV vs Regular ASV¶
Both are ASV entries in AnnotationSession.sessionVersions[]. The difference is audit.action:
- Regular ASV: Created by the annotator saving the form.
action: "annotatorSave"oraction: "sessionComplete". Audit points to the annotator's InvestigatorId. - Transition ASV: Created atomically at publish time by the system, per the admin's PublishDecision.
action: "adminTransition". Audit points to the admin's InvestigatorId. Contains carried-forward, mapped, or cleared annotations per the admin's configuration.
There is no "draft ASV" concept. Transition ASVs are created atomically at publish time or not at all. The admin's decisions in the publish wizard determine what the ASV will contain, but the ASV itself is only created at the moment of publish.
Session Handling: Completed vs In-Progress¶
The publish wizard separates completed and in-progress sessions because the admin's instincts differ:
In-progress sessions (annotator is actively working, disruption is expected):
| Option | Effect |
|---|---|
| Don't update | Session continues with current questions. No transition. |
| Map answers | Answer mapped. Optionally: ask to review. |
| Re-answer | Answer cleared. Previous in history. |
Completed sessions (annotator considers work done, reopening is a bigger imposition):
| Option | Effect |
|---|---|
| Leave as they are (default) | Sessions stay complete. No disruption. |
| Map (keep complete) | Answers silently mapped. |
| Map + reopen for review | Answers mapped. Session reopened. Annotator must acknowledge and re-complete. |
| Reopen for re-answer | Answers cleared. Session reopened. Annotator must answer and re-complete. |
Admin can filter which completed sessions to affect by answer value — for example, only reopen sessions where annotator selected "Weight drop" (now invalid), leaving "CCI" and "Fluid percussion" sessions alone.
Multi-Question Conflict Resolution¶
Per-question decisions are conditional on session transition. "Don't update sessions for this question" means: don't trigger a transition because of this question. But if the session transitions anyway (due to another question), this question's annotation must still be validated against the new AQ version (per the consistency invariant).
After all per-question decisions, the system computes the effective per-session outcome. Sessions where per-question decisions conflict are flagged in Step 4 of the wizard.
Example conflict: Question A says "don't update" but Question B says "reopen for re-answer." Session #42 has answers for both. Because Question B requires transitioning Session #42, Question A's annotation is now in the context of the new AQ version. If Question A's new version removed the option the annotator selected, Question A has an invalid answer in the transitioned session.
Resolution: The admin must provide a handling decision for Question A in the context of Session #42's transition — either map the answer or ask to re-answer. Publish is disabled until all conflicts are resolved.
3.3 Answer Mapping Configuration¶
When a publish involves option changes (removal, rename, merge) and annotations exist, the admin configures per-option handling:
"Weight drop" (23 annotations)
- Map to: [Select option... ]
- Ask annotator to re-answer
- Keep original answer (only if option still exists)
Key rules:
- Per-option granularity: Each removed/changed option with annotations gets its own decision
- Mapping + review: Mapping can be combined with "ask annotators to review" — the mapped answer is pre-filled but the annotator must acknowledge it (dismissible alert)
- "Keep" constraint: "Keep original answer" is only available if the answer value is objectively valid in the new AQ version. If the option was removed and the annotator selected it, carry-forward is not available — the admin must choose map or re-answer
- "Use previous answer" on annotator side: Only shown if the previous answer is still a valid option in the new version. If previous option was renamed and admin configured mapping, the mapped value is pre-filled
3.4 Breaking Change Detection¶
PR2398 specifies BreakingChange: boolean on each AQVersion. PR2461 also includes this (implementation plan line 49). Breaking change transitivity (v2→v5 is breaking if any intermediate version was breaking) is computed during QSV transitions.
Integration point: The publish flow in PR2461 should prompt the admin to set breakingChange when creating a new AQVersion. The UI for this should be in the publish review dialog (Phase 5, Assign view).
4. Reconciled Data Model Summary¶
This is the authoritative merged model combining PR2398's entity design with PR2461's PQS/SQS additions.
4.1 Collections¶
| Collection | Aggregate Root | Phase | Notes |
|---|---|---|---|
pmProject (extended) |
Project | 1 | + DraftQuestions[], ProjectQuestionSet, Stage.StageQuestionSet, DraftSnapshots[] |
pmAnnotationQuestion (NEW) |
AnnotationQuestion | 1 | Published questions with draft field + publishedVersions[] |
pmStudy (extended) |
Study | 1 | + QuestionVersionId on Annotations |
pmAnnotation (FUTURE) |
Annotation | Future | Extracted from Study — enables per-annotation auto-save + AV history |
pmAnnotationSession (FUTURE) |
Session | Future | Extracted from Study — enables ASV pinning + session versioning |
4.2 Entity Relationship Flow¶
DraftQuestion (on Project)
│
│ [First Publish]
▼
AnnotationQuestion (pmAnnotationQuestion)
│
│ [Each publish creates]
▼
AQVersion (embedded in AQ.publishedVersions[])
│
│ [Referenced by]
▼
PQSVersion (embedded in Project.ProjectQuestionSet.versions[])
│ Contains: orderedQuestionRefs [{questionId, versionId}]
│
│ [Subsetted by]
▼
SQSVersion (embedded in Stage.StageQuestionSet.versions[])
│ Contains: pqsVersionId + questionIds (unordered)
│
│ [Determines questions shown in]
▼
Annotation Form → Annotation (on pmStudy, records QuestionVersionId)
│
│ [FUTURE: when extracted to pmAnnotation]
▼
AnnotationVersion (AV) → pinned by → SessionVersion (ASV)
4.3 PublishDecision Entity¶
The PublishDecision entity captures admin decisions about how to handle existing annotations when publishing a new AQ version. It exists in two forms:
DraftPublishDecision (mutable, two locations)¶
Stored on AQ.draft.draftPublishDecision for edits to existing published questions, OR on DraftQuestion.draftPublishDecision for replacement drafts with lineage (replacesAnnotationQuestionId != null). Same schema in both cases:
DraftPublishDecision {
classification: "non-breaking" | "breaking" | null
handling: "no-action" | "keep-sessions" | "map-answers" | "re-answer" | null
answerMappings: [{fromValue, toValue, requiresReview}]?
// No session scope — that's stage-specific, decided at publish time
}
PublishDecision (on AQVersion, immutable)¶
Created at publish time by promoting the DraftPublishDecision. Stores the complete decision record:
PublishDecision {
questionId: Guid
aqVersionId: Guid // the new AQVersion
previousAqVersionId: Guid? // the version being superseded
classification: "non-breaking" | "breaking"
classificationSource: "admin-confirmed" | "system-detected"
handling: "no-action" | "keep-sessions" | "map-answers" | "re-answer"
answerMappings: [
{
fromValue: string // old answer value
toValue: string // mapped new value
requiresReview: bool // annotator must acknowledge
}
]?
inProgressSessionHandling: handling
completedSessionHandling: handling
completedSessionFilter: {
filterType: "none" | "all" | "by-answer"
answerValues: string[]?
}
decidedBy: Guid // InvestigatorId of admin
decidedAt: DateTime
changeNote: string?
}
Where Stored¶
AQVersion.publishDecisions: PublishDecision[]— immutable record of what was decidedSQSVersion.publishDecisionSummary— summary for session transition engine- Transition
ASV.transitionDecisionRef— points to the SQSVersion that triggered it
4.4 VersionAudit Entity¶
Present on all versioned entities (AQVersion, PQSVersion, SQSVersion, ASV, AV). Provides cascading provenance through the entire version chain:
VersionAudit {
initiatedBy: Guid // InvestigatorId
createdAt: DateTime
action: string // "publish", "adminTransition",
// "annotatorSave", "sessionComplete", etc.
reason: string? // change note or auto-generated
triggeredBy: { // what caused this version
entityType: string // "SQSVersion", "AQVersion", etc.
entityId: Guid
versionId: Guid
}?
}
Cascade Example¶
Admin Chris publishes a stage:
AQVersion v3 (TBI Model Type)
audit: { initiatedBy: Chris, action: "publish",
reason: "Consolidating TBI models" }
PQSVersion v4
audit: { initiatedBy: Chris, action: "publish",
triggeredBy: AQVersion v3 }
SQSVersion v3 (Data Extraction)
audit: { initiatedBy: Chris, action: "publish",
triggeredBy: PQSVersion v4 }
ASV v2 (Alice's session, Study #42)
audit: { initiatedBy: Chris, action: "adminTransition",
triggeredBy: SQSVersion v3,
reason: "Mapped 'Weight drop' -> 'CCI'" }
AV v2 (Alice's annotation for TBI Model Type)
audit: { initiatedBy: Chris, action: "adminTransition",
triggeredBy: AQVersion v3,
reason: "Answer mapped: 'Weight drop' -> 'CCI'" }
When Alice later reviews:
AV v3 (Alice confirms the mapped answer)
audit: { initiatedBy: Alice, action: "annotatorSave",
triggeredBy: ASV v2 }
4.5 Cross-Reference Guide¶
| PR2398 Concept | PR2461 Equivalent | Notes |
|---|---|---|
| DraftAQ | DraftQuestion | Same concept. PR2461 adds type: "draft"\|"published" discriminator in PQS references |
| AQ | AnnotationQuestion | Same. PR2461 splits structural props (frozen) from content (in draft field) |
| AQVersion | AQVersion (in publishedVersions[]) |
Same. PR2461 uses GUID _id instead of composite identity |
| QS (QuestionSet) | ProjectQuestionSet | PQS is the project-level equivalent of PR2398's QS |
| QSV (QuestionSetVersion) | PQSVersion + SQSVersion pair | PR2398's single QSV splits into project-level PQS version (ordering + version refs) and stage-level SQS version (question subset) |
| pendingChanges (on AQ) | AQ.draft field | Same purpose. PR2461 names it draft instead of pendingChanges — the autosave target |
| pendingAnswer (on Annotation) | Not yet implemented | Deferred until annotation extraction from Study |
| Annotation (own aggregate) | Annotation (still embedded on Study) | PR2398 plans extraction; PR2461 defers this |
| AnnotationSession (own aggregate) | AnnotationSession (still embedded on Study) | Same deferral |
| AV (AnnotationVersion) | QuestionVersionId on Annotation | Simplified — only records which AQVersion, not full AV entity |
| ASV (SessionVersion) | Not yet implemented | Deferred to Phase 8 |
| Resolved Question Set | Not yet implemented | Will be needed for Phase 8 ASV reconstruction |
| PAC (Parent Annotation Condition) | Existing Target/answerOptionFilters logic |
Same conditional logic, but will need formalisation for cross-stage consistency |
| crossStageConsistency (D51) | Not yet implemented | Future — when cross-stage PAC tracking is needed |
| Breaking change transitivity | Planned for Phase 8 | PR2398 algorithm is authoritative |
| (no equivalent) | replacesAnnotationQuestionId on DraftQuestion |
New field linking a replacement draft to the original AQ being replaced. Enables annotation migration at publish time (Phase 8). See mental-model-review-decisions.md Section 6 |
| (no equivalent) | DraftPublishDecision on AQ.draft / DraftQuestion |
Mutable pre-publish decision. Promoted to immutable PublishDecision on AQVersion at publish time. See Section 4.3 |
| (no equivalent) | PublishDecision on AQVersion |
Immutable record of admin decisions about annotation handling. See Section 4.3 |
| (no equivalent) | VersionAudit on all versioned entities |
Cascading provenance audit trail. See Section 4.4 |
| (no equivalent) | publishDecisionSummary on SQSVersion |
Summary of publish decisions for session transition engine |
5. Recommendations for Implementation Plan Updates¶
5.1 Phase 1 (Data Model Foundation) — Add¶
- Add
breakingChange: boolto AQVersion entity (already in plan line 49, but confirm in data model diagram) - Add
changeReason: string?to AQVersion entity (already in plan line 253) - Add
DraftPublishDecisionto AQ.draft schema (see Section 4.3) - Add
replacesAnnotationQuestionId: Guid?to DraftQuestion entity (see Section 4.5) - Add
PublishDecisionto AQVersion entity (see Section 4.3) - Add
VersionAuditto all versioned entities: AQVersion, PQSVersion, SQSVersion, ASV, AV (see Section 4.4) - Add
publishDecisionSummaryto SQSVersion - Design
AnnotationandAnnotationSessiondomain entities as if they were separate aggregates, even though they remain embedded on Study for now — this future-proofs the extraction - Document the composite identity pattern from PR2398 (D57) as a design note, even though we use GUIDs for MongoDB pragmatism
Phases 1-7 Mitigation: Blocking Publishes to Annotated Stages¶
Publishing new AQ versions to stages that have annotation sessions is blocked until Phase 8 is implemented. Without annotation versioning (session transitions, ASVs, PublishDecisions), the existing tree-shaking annotation replacement (AddAnnotations) silently destroys previous answers with no history. This is worse than the current system because the admin has a false sense of control from the publishing ceremony.
Rules during Phases 1-7:
- Admin CAN publish questions to a stage for the first time (initial setup)
- Admin CAN publish to stages with no annotation sessions
- Admin CANNOT publish a new AQ version to a stage with existing annotations
- The system shows: "This stage has active annotations. Updating questions on annotated stages requires annotation versioning, which will be available in a future update."
- Admin can still: edit drafts, publish to other unannotated stages, use full Design/Assign/Preview workflow
This is a per-publish check, not a permanent lock — each time the admin attempts to publish, the system checks whether the target stage has annotation sessions.
5.2 Phase 3 (Backend API) — Add¶
- The annotation form question resolution chain (SQS → PQS → pmAnnotationQuestion) needs a dedicated query/service, not just CRUD endpoints
- When saving annotations, write
QuestionVersionId(the AQVersion GUID) alongside the existingQuestionId - Add
BreakingChangeflag to the "create new AQVersion" endpoint request body
5.3 Phase 7 (Annotation Form v2) — Clarify¶
- The annotation form must resolve questions via the SQS → PQS → AQ chain for migrated projects
- Conditional logic (PAC) uses
answerFiltersfrom the pinned AQVersion, not the latest version - The form should record
QuestionVersionIdon each annotation when saving
5.4 Phase 8 (Admin Decision Framework) — Expanded into 5 Sub-Phases¶
Phase 8 is the largest phase in the implementation plan. It is broken into 5 sub-phases, each of which is independently testable:
Phase 8.1: Annotation Impact Assessment Engine¶
Compute annotation counts, answer distributions, and session counts on demand. This is the data layer that powers all subsequent UI and decision-making:
- Count annotations by QuestionId (project-wide, not per-stage)
- Count distinct studyId values per question
- Group latest AVs by answer value (answer distribution)
- Group annotations by aqVersionRef (annotations per AQ version)
- Group sessions by latest ASV status (completed vs in-progress counts)
- All metrics computed on-demand (not cached), triggered by question selection in properties panel or publish wizard opening
Phase 8.2: Impact & Mapping Configuration (Design View)¶
The Impact & Mapping section in the Design view properties panel, plus the DraftPublishDecision entity:
- Auto-expand Impact & Mapping section when a published AQ with annotations is edited
- Show annotation counts and answer distribution from Phase 8.1
- Admin configures: classification, handling (keep/map/re-answer), answer mappings, change note
- Store as
DraftPublishDecisiononAQ.draft.draftPublishDecision(for edits to existing AQs) - Store as
DraftPublishDecisiononDraftQuestion.draftPublishDecision(for replacement drafts withreplacesAnnotationQuestionIdlineage) - DraftPublishDecision is mutable — can be changed any time before publish
Phase 8.3: Publish Wizard with Impact¶
Replace the single publish dialog with a 5-step wizard. Concurrency re-validation at publish time (see Section 3.1.1):
- Step 1: Review Changes — orientation, grouped by source (own edits vs other-stage updates), first-publish info card
- Step 2: Confirm Impact — per-question classification radio ("does not affect" / "may affect"), pre-populated from DraftPublishDecisions, auto-confirmed for zero-annotation questions, disabled until all confirmed
- Step 3: Configure Answer Handling (conditional) — per-question decisions with answer distribution, mapping config, separate sections for in-progress and completed sessions, pre-populated from Design-time config
- Step 4: Resolve Conflicts (conditional) — multi-question conflict detection and resolution (see Section 3.2)
- Step 5: Review & Publish — session impact summary, final review, change notes summary
- Concurrency re-validation: snapshot at wizard open, re-validate at publish, abort and refresh if state changed
Phase 8.4: Session Transition Engine¶
Transition ASVs created atomically at publish time per PublishDecision. Per-question decisions applied to sessions.
Breaking Change Detection¶
Breaking changes are always classified by the admin, never auto-computed. The system cannot determine semantic intent — a text change, new option, or help text change can all recontextualise the question in ways that affect how existing answers should be interpreted. The system's role is limited to:
- Detecting objectively invalid states (Track 2): an option was removed or renamed, and annotators have selected it. These MUST be decided before publish — the wizard disables the Publish button until all are resolved.
- Requiring explicit confirmation for all changes (Track 1): every change to a published question with annotations requires a per-question radio selection: "This change does not affect existing answers" / "This change may affect existing answers". No passive defaults. Questions with zero annotations are auto-confirmed.
When the admin selects "may affect", the question expands into the Track 2 decision flow (configure answer handling).
The breakingChange: boolean field on AQVersion records the admin's final classification for audit and transitivity computation. It is metadata, not a user-facing concept — the UI never uses the phrase "breaking change".
Breaking Change Transitivity¶
A version transition from v_old to v_new is breaking if ANY intermediate version in the chain was marked as breaking:
This ensures that even if intermediate versions were not published to a particular stage, the transitive breaking status is correctly computed when a session transitions across multiple versions.
ASV Reconstruction Algorithm¶
When the admin publishes a stage and the Session Transition Engine processes affected sessions, each annotation in the session is classified against the new SQS version:
| Classification | Condition | Action |
|---|---|---|
| Carried forward | Answer is objectively valid in new AQ version (option still exists, type unchanged) AND admin confirmed "does not affect" | Copy annotation as-is to transition ASV |
| Mapped | Admin configured answer mapping (per-option) | Apply mapping, copy mapped value to transition ASV. If "ask to review" flag set, mark annotation for annotator review. |
| Cleared for re-answer | Admin selected "ask annotators to re-answer" | Clear answer value, store previous answer in history. Annotator must provide new response. |
| New | Question added to stage (not in previous SQS version) | No annotation exists — annotator will answer on next session. |
| Removed | Question removed from stage | Annotation preserved in session history but not carried to transition ASV. |
| Newly visible | Question was hidden by conditional logic (parent answer changed), now visible | No annotation exists — treated as new for this session. |
The reconstruction walks the question tree top-down, evaluating parent-answer conditions (PAC) at each level. If a parent's answer was mapped, the mapped value determines child visibility.
Implementation Checklist¶
- Implement the two-track classification model in the publish wizard (Track 1: subjective, Track 2: objectively invalid)
- Implement session versioning (ASV) as part of this phase — not before
- Create transition ASVs with
action: "adminTransition", audit pointing to admin's InvestigatorId - Per-question decisions: carry-forward (if valid), map answers, clear for re-answer
- Completed sessions: default to "leave as-is", with admin options for map/reopen/re-answer
- In-progress sessions: follow per-question decisions directly
- Answer mapping: per-option granularity, mapping can be combined with "ask to review"
- Implement breaking change transitivity formula for cross-version transitions
- Cross-stage PAC consistency (D51, D53, D54, D55) can be deferred beyond Phase 8 unless reconciliation is being implemented concurrently
Phase 8.5: Annotator-Facing Version Transition¶
The annotator's experience when questions change:
- Dismissible inline alerts on questions that were updated — "This question was updated -- please review"
- Alert shows previous answer, dismissing removes it entirely (no lingering visual noise)
- Dismissed state persists across page loads (stored on session)
- Session cannot be marked complete until all flagged questions are dismissed or answered
- Re-answer flow: answer cleared, previous answer shown in history, annotator must provide new response
- "Use previous answer" button: only available if previous answer is still valid in new version; not shown if option was removed; pre-fills mapped value if admin configured mapping
- Answer history per question (all AVs) and session history (all ASVs) always available to annotator
5.5 New Phase Consideration: Annotation Extraction¶
Between Phase 8 and Phase 9 (or as a sub-phase of Phase 9), consider adding:
- Phase 8.5: Annotation & Session Extraction
- Extract annotations from
pmStudytopmAnnotationcollection - Extract sessions from
pmStudytopmAnnotationSessioncollection - Remove Study back-references per D50-revised
- Enable per-annotation
pendingAnswerauto-save - Enable full AV (AnnotationVersion) history
- Enable ASV-based session pinning
This is a prerequisite for the full PR2398 annotation versioning model and should happen before production migration (Phase 9) if full audit trail is required at launch.
6. Open Questions¶
| # | Question | Impact | Suggested Resolution |
|---|---|---|---|
| 1 | Should annotation extraction (pmAnnotation, pmAnnotationSession) be part of QM v2, or a separate workstream? | High — determines whether sessions can be pinned to SQS versions | Defer to after QM v2 Phase 7. Implement as Phase 8.5 if session versioning is needed before production rollout. |
| 2 | During the dual-model period, should new annotations on migrated projects record QuestionVersionId even without full AV history? |
Medium — partial audit trail | Yes. Record QuestionVersionId immediately. Full AV history can be backfilled later. |
| 3 | Should the publish review dialog include the breakingChange flag UI from Phase 5, or defer to Phase 8? |
Low — admin convenience | Include a simple toggle in the publish review dialog (Phase 5). The transitivity computation is Phase 8. |
| 4 | PR2398 specifies Scope and OwnerId on AQ for cross-project sharing. PR2461 includes scope and ownerProjectId. Should these be populated during migration? |
Low — future feature | Yes, default to scope: "project", ownerProjectId: projectId. Zero cost now, enables future sharing. |
| 5 | Does the annotation form need to handle SQS version transitions mid-session (annotator is working, admin publishes)? | Medium — UX concern | For Phases 1-7: no. Form loads SQS version at session start and uses it for the duration. If admin publishes mid-session, annotator sees new questions on next session open. Phase 8 adds formal transition handling. |
| 6 | Should system questions be versioned independently of project admin publishes, or folded into the same PQS publish ceremony? | High — affects migration and publish flow | See Section 7. System questions should be versioned by a system-level publish triggered on project creation or SystemQuestionVersion bump, separate from admin publishes. |
| 7 | How should SystemQuestionVersion bumps be handled for existing migrated projects? |
Medium — migration concern | A system-admin upgrade workflow performs a dry-run diff, lets the admin target all or a subset of projects, and then runs the upgrade immediately or marks projects for lazy completion on next QM load. See Section 7.5. |
| 8 | Should the system question hierarchy (control → label → lookup chain) be represented as DraftQuestions during migration, or jump straight to AnnotationQuestions? | Low — migration implementation detail | Jump straight to AnnotationQuestion. System questions are never in "draft" state — they are system-defined and published from birth. See Section 7.3. |
7. System Question Handling¶
System annotation questions have unique constraints that require explicit treatment in the new versioning architecture. They cannot be handled as a special case of custom questions — they are a fundamentally different lifecycle.
7.1 What Makes System Questions Different¶
| Aspect | Custom Questions | System Questions |
|---|---|---|
| Created by | Project admin in Design view | System, automatically on stage creation |
| 17 hardcoded GUIDs | No — new GUID per question | Yes — AnnotationQuestion.cs lines 307-386 define static GUIDs |
| Editable by admin | Yes (text, options, etc.) | No — content is system-defined |
| Deletable | Yes (from draft PQS) | No — always present in data extraction stages |
| Structural role | Extend the annotation form | Define the annotation form hierarchy (control → label → lookup → custom) |
| Parent-child | Parent to other custom questions, or child of system questions | Form the mandatory hierarchy skeleton that custom questions attach to |
| Version triggers | Admin edits + publishes | Project.SystemQuestionVersion bump (platform-level change) |
| OutcomeData coupling | None | Direct — system question values populate OutcomeData properties (AverageType, ErrorType, GreaterIsWorse, Units, NumberOfAnimals) |
| Data export coupling | Indirect (via annotations) | Direct — OutcomeDataFormatRowWriter reads system question annotations to populate export columns |
| Cross-category hierarchy | Within one category | Span categories (e.g., Cohort lookups reference DMI, Treatment, and Outcome labels) |
7.2 The 17+1 System Questions¶
All GUIDs are hardcoded in AnnotationQuestion.cs and must be preserved as identity through any migration.
| Category | Question | GUID | Type | Parent |
|---|---|---|---|---|
| DMI | DMI Control | b18aa936-... |
Boolean | root |
| DMI | DMI Label | bdb6e257-... |
Label (Dropdown) | DMI Control |
| Treatment | Treatment Control | d04ec2d7-... |
Boolean | root |
| Treatment | Treatment Label | b02e3072-... |
Label (Dropdown) | Treatment Control |
| Outcome | OA Label | dbe2720c-... |
Label (Dropdown) | root |
| Outcome | Average Type | 3a287115-... |
Dropdown | OA Label |
| Outcome | Error Type | 8dbea59f-... |
Dropdown | OA Label |
| Outcome | Greater Is Worse | 45351e04-... |
Boolean | OA Label |
| Outcome | Units | 66eb1736-... |
Textbox | OA Label |
| Outcome | PDF Graphs | 016278e8-... |
Lookup (Hidden) | OA Label |
| Cohort | Cohort Disease Models | ecb550a5-... |
Lookup | root |
| Cohort | Cohort Treatments | a3f2e5bb-... |
Lookup | root |
| Cohort | Cohort Outcomes | 12ecd826-... |
Lookup | root |
| Cohort | Cohort Label | 62c852ad-... |
Label (Dropdown) | Cohort DMs + Treatments + Outcomes |
| Cohort | Number of Animals | 83caa64f-... |
Integer Textbox | Cohort Label |
| Experiment | Experiment Cohorts | e7a84ba2-... |
Lookup | root |
| Experiment | Experiment Label | 7c555b6e-... |
Label (Dropdown) | Experiment Cohorts |
| Hidden | PDF References | 7ee21ff9-... |
Lookup | root |
7.3 System Questions in the New Data Model¶
System questions must exist in pmAnnotationQuestion alongside custom questions, but with different lifecycle rules.
On Migration (Phase 2)¶
For each project with data extraction stages:
- Create
AnnotationQuestiondocuments inpmAnnotationQuestionfor each of the 17+1 system questions _id: the hardcoded GUID (identity preserved)projectId: the project's IDsystem: truecategory: from the system question definitionparentQuestionId: from the system question hierarchydataType: from the system question definitiongroupAsSingle: from the system question definitiondraft: null(system questions have no pending draft edits)publishedVersions: [AQVersion v1]— content fromSystemQuestionCreatorsfactory- Do NOT create DraftQuestions for system questions — they skip the draft phase entirely
- Include system question AQVersionIds in the initial PQS v1
- Include appropriate system questions in each stage's SQS v1 (data extraction stages get all; other stages may not)
On New Project Creation (Post-Migration)¶
When a new project is created with data extraction stages:
- Create
AnnotationQuestiondocuments for all system questions (same as migration) - Create initial PQS version including system question refs alongside any DraftQuestion refs
- System questions appear in the Design view as read-only nodes (locked icon, no edit/delete/drag)
SystemQuestionVersion-Dependent Content¶
The Outcome Error Type question's options change based on Project.SystemQuestionVersion:
- v0: answerOptionFilters reference certain conditional parents
- v1+: Different conditional option parents
When migrating, the AQVersion v1 content must match the project's current SystemQuestionVersion. If a project has SystemQuestionVersion = 0, the Error Type question gets v0 content; if SystemQuestionVersion = 1, it gets v1 content.
7.4 System Questions in the UI¶
Design View¶
- System questions render in the tree as read-only nodes with a lock icon
- Cannot be edited, deleted, reordered, or dragged
- Their children (custom questions) CAN be edited/reordered
- System questions define the anchor points that custom questions attach to:
- Category Study: custom questions are root-level
- Category DMI: custom questions parent to DMI Control (with conditional answers required)
- Category Treatment: custom questions parent to Treatment Control (with conditional answers required)
- Category Outcome: custom questions parent to OA Label
- Category Cohort: custom questions parent to Cohort Label
- Category Experiment: custom questions parent to Experiment Label
- Category Hidden: NO custom questions allowed
Assign View¶
- System questions are auto-included for data extraction stages and cannot be unassigned
- Shown with a lock icon and "System" badge
- Checkboxes are checked and disabled
- Custom questions can be assigned/unassigned freely (subject to parent integrity — if a custom question is a child of a system question, the system question is always present)
Publish Flow¶
- System questions are never part of admin publish actions — they don't have draft changes
- When admin publishes a stage, only custom question changes create new AQVersions
- System questions' AQVersionIds remain unchanged in the new PQS version (carried forward)
- Exception: if
SystemQuestionVersionhas bumped, system question AQVersions may have been updated by an explicit system-admin upgrade run (see 7.5)
7.5 SystemQuestionVersion Bumps¶
Project.SystemQuestionVersion is an integer that can be bumped when the platform evolves the system question definitions (e.g., adding new options to Error Type).
Current behaviour: The system dynamically generates questions using SystemQuestionCreators[guid]() at runtime, passing project.SystemQuestionVersion. The generated content varies by version.
New behaviour with QM v2: System questions are persisted in pmAnnotationQuestion with explicit AQVersions. A SystemQuestionVersion bump must:
- Detect which system questions' content differs between the old and new version
- Create new AQVersions on the affected
AnnotationQuestiondocuments inpmAnnotationQuestion - Create a new PQS version on the project with updated system question versionIds
- Do NOT auto-update SQS versions — stages continue using their current SQS version until the admin publishes (or until Phase 8 transition handling kicks in)
- Show indicator on affected stages in the Assign view: "System questions updated — publish to apply"
Implementation model:
| Step | Behaviour |
|---|---|
| 1. Detect release | A new platform release increments Project.SystemQuestionVersion semantics for one or more system questions. |
| 2. Run dry-run diff | A system-admin tool computes which system questions changed and which migrated projects are affected. |
| 3. Select scope | The system admin chooses all projects or a subset of projects. |
| 4. Choose execution mode | Per run, the admin chooses either Immediate upgrade now or Mark pending for lazy completion on next QM load. |
| 5. Create new versions | For selected projects, the upgrade creates new system AQVersions and appends a new PQS version with the updated refs. |
| 6. Leave SQS unchanged | Stages continue using their current SQS versions until an admin republishes the stage (or later Phase 8 transition handling applies). |
| 7. Audit | The upgrade records execution metadata, including dry-run output, selected projects, execution mode, and results. |
Recommendation: Use the explicit system-admin upgrade workflow as the default operating model. It is safer than a silent background process, gives the admin control over rollout scope, and still supports lazy per-project completion when that is the preferred operational mode.
7.6 System Questions and the Annotation Form¶
The annotation form's interaction with system questions is critical because of the OutcomeData coupling.
Current flow:
1. Form loads system questions from Project (dynamically generated)
2. Annotator answers system questions (labels, controls, lookups)
3. On save, system question annotations are written to pmStudy.Annotations[]
4. Data export reads system question annotations to populate OutcomeData properties
QM v2 flow (migrated project):
1. Form resolves system questions via SQS → PQS → pmAnnotationQuestion (same chain as custom questions)
2. System questions are rendered with the content from their pinned AQVersion (via PQS version)
3. Annotator answers system questions (unchanged UX)
4. On save, annotations record QuestionVersionId pointing to the system question's AQVersion
5. Data export reads annotations — QuestionId still matches the hardcoded GUID, so OutcomeData population is unchanged
Key invariant: The hardcoded GUIDs (AnnotationQuestion._id) are the stable identity. OutcomeData and data export code uses these GUIDs to find annotations. The versioning layer (AQVersions, PQS, SQS) is transparent to data export — it only needs the QuestionId, not the QuestionVersionId.
7.7 Placement Rules and System Questions¶
The AnnotationQuestionPlacementValidator enforces category-based rules. In the new model:
| Rule | Current Enforcement | QM v2 Enforcement |
|---|---|---|
| Custom questions in DMI must parent to DMI Control | Frontend + AnnotationQuestionPlacementRules.cs |
Same — validated on DraftQuestion creation and on publish |
| Custom questions in Treatment must parent to Treatment Control | Same | Same |
| Custom questions in OA must parent to OA Label | Same | Same |
| Custom questions in Cohort must parent to Cohort Label | Same | Same |
| Custom questions in Experiment must parent to Experiment Label | Same | Same |
| Hidden category prohibits custom questions | Same | Same — DraftQuestion creation blocked for Hidden category |
| DMI and Treatment require control parameter (conditional answers) | Frontend-only currently | Must be enforced in backend validation during publish (Phase 3) |
Migration concern: The 32 known production violations of placement rules (from formal-specification.md characterization tests) are all in custom questions, not system questions. System questions always satisfy placement rules by construction (their hierarchy is hardcoded). The migration must tolerate these custom question violations while preventing new ones.
7.8 Recommendations for Implementation Plan¶
Phase 1 (Data Model Foundation)¶
- Add
system: boolfield toAnnotationQuestionentity - System questions in
pmAnnotationQuestionmust use their hardcoded GUIDs as_id - System questions must NOT appear in
Project.DraftQuestions[]— they bypass the draft phase - PQS
QuestionReferencefor system questions always hastype: "published"(never"draft") - Add
SystemQuestionVersion-aware content generation for initial AQVersion creation
Phase 2 (Migration Infrastructure)¶
-
QuestionMigrationServicemust handle system questions specially: - Create
AnnotationQuestiondocs withsystem: trueand hardcoded GUIDs - Generate AQVersion v1 content using
SystemQuestionCreatorswith the project'sSystemQuestionVersion - Preserve the exact parent-child hierarchy from the system question definitions
- System questions are included in PQS v1 but ordered before custom questions within each category
- System questions that don't yet exist as persisted entities (because they were dynamically generated) must be created during migration — this is a CREATE, not an UPDATE
- Validate that all 17+1 system question GUIDs are present after migration
Phase 3 (Backend API)¶
- Question resolution service must handle
system: truequestions identically to custom questions in the SQS → PQS → AQ chain — no special-casing at query time - The "create AQVersion" endpoint must reject calls for system questions (only the system can version them)
- The "delete question" endpoint must reject calls for system questions
- Add a
SystemQuestionVersioncheck on QM page load (lazy version bump handling, see 7.5)
Phase 4 (Design View)¶
- System questions render as read-only locked nodes in the tree
- Cannot be selected for editing in the properties panel (or panel shows read-only system info)
- Cannot be targets for drag-and-drop (neither source nor insertion target for reordering)
- Custom questions CAN be dragged to become children of system questions (this is the primary hierarchy mechanism)
Phase 5 (Assign View)¶
- System questions shown with lock icon + "System" badge, checkbox checked and disabled
- "Unassign" action disabled for system questions
- System questions not included in the "pending changes" count (they don't change via admin action)
Phase 7 (Annotation Form)¶
- Annotation form resolves system questions through the same SQS → PQS → AQ chain
-
OutcomeDatapopulation continues to useQuestionId(hardcoded GUID) — unaffected by versioning - Data export code (
OutcomeDataFormatRowWriter) continues to work — it queries byQuestionId, notQuestionVersionId