Skip to content

Read-only audit of #2574 at 1b93cbbc4. Saved verbatim as evidence for the harvest map. Feature implementation is on hold; nothing was ported, rebased or closed.

Audit: #2574 (QM v2 PR-C): migration, ADR-009 cutover and API

Read-only audit, 5 October 2026. Head 1b93cbbc4, base feat/qm-v2-b-admin-services (PR-B head 271290115). The specifications were read on main (7673ed0d3) in the pr4061 worktree. Nothing was checked out, fetched, committed or commented.

Path abbreviations used in the evidence column:

  • PMC = src/libs/project-management/SyRF.ProjectManagement.Core
  • PMA = …/SyRF.ProjectManagement.Application
  • PMD = …/SyRF.ProjectManagement.Mongo.Data
  • PMT = …/SyRF.ProjectManagement.Core.Tests
  • API = src/services/api/SyRF.API.Endpoint
  • APIT = …/SyRF.API.Endpoint.Tests

Unless stated otherwise, evidence is at 874a421 (C's only feature commit). None of the cited files change between 874a421 and the head 1b93cbb, except API/Program.cs.

1. Scope and state

1.1 What the PR contains and its real size

Layer Files Lines added How it was measured
GitHub file list (pulls/2574/files) 604 +34,231 / −3,992 Diff against the PR-B merge base. It includes main merges
C's own work: commit 874a421fb against its parent 271290115 (PR-B head) 225 (139 added, 85 modified, 1 deleted) +28,755 / −861 git diff 271290115 874a421fb
From main merges (4e73a29 … 1b93cbb, 18–24 April) 380 — 260 under docs/planning, 14 under .planning, plus workflows and src/ changes from main
Generated clients 0 — Only .generated-checksums.json; the web client changes are in PR-D (#2575)
Embedded #2467 (SignalR active-reviewer tracking) 73 of the 225 — 49 are byte-identical copies of #2467 branch blobs from 10–17 April; 24 are mixed files carrying #2467 edits plus QM edits
Embedded #2543 (ADR-009 extraction) 22 of #2543's 30 files, 14 of them not already counted as #2467 — 11 are byte-identical to #2543's merge 6506f7e28
C's own QM work 138 files, plus QM edits inside the 24 mixed files +16,966 (8,813 source, 8,153 tests) 225 minus the #2467 and #2543 sets

C's own QM work falls into five areas:

  • Migration (PMC/PMA, about 2,200 source lines): ProjectQuestionMigrationDomainService, ProjectQuestionMigrationApplicationService, ReviewStateMigrationDomainService, MigrationValidationService, MigratedStudyReadModelAssembler, ExtractedAnnotationLegacyMapper, ReconstructiveRollbackService, MigrationReport, and Study's extracted-state switch.
  • Cutover (PMC/PMD):
  • readers (StageImpactReader, ExportSchemaObservationReader and three stage-transition readers);
  • ProjectStatsAggregate and ProjectStatsService (pmProjectStats);
  • StageTransitionWorker with its workload service and StageTransitionBackgroundService;
  • repositories for PR-A's extracted aggregates;
  • the StudyRepository rewrite (+1,312 lines).
  • Review writes (PMC/API):
  • MigratedReviewSubmissionService and MigratedReviewMutationPlanner;
  • OptimisticConcurrencyExceptions;
  • the GuardLegacyReviewPath change to ReviewSubmissionService;
  • per-project branching in ReviewController.
  • API:
  • QuestionManagementV2Controller (17 endpoints, 1,031 lines, with its DTOs);
  • StagePublishApplicationService;
  • MigratedStudyDtoAssembler.
  • Export: ExportSpec (modes and selectors), ExportSchemaSidecar, ExportQuestionCatalog, and version-aware header writers.

1.2 Isolating the embedded #2467 files

The known fact said 75 files. It is approximately confirmed: 73 files, with one correction.

  • 2467 is not unmerged. It merged into main on 30 August 2026 (0d943a34d, 100 commits), after

    a redesign that replaced ActiveReviewSession and ExpiredReviewSession with SlotReservation and ReviewerPresence.
  • Of C's 49 verbatim copies, 35 differ from main today, 7 no longer exist there and 7 are identical.
  • The PR body attributes five files to #2467:
  • AssignmentBlockedReason
  • ReviewerAssignmentState
  • ReviewerReservationState
  • ReviewerSessionState
  • StageAssignmentTally

Those five are in fact QM's own. They come from #2461 commit 3562ff913, and no #2467 commit touches them.

Directories of the 73 #2467-derived files (V = verbatim copies, M = mixed):

  • Shared libraries:
  • SharedKernel/Interfaces: 3 V
  • Mongo.Common: 4 V, 1 M; its tests: 1 V
  • WebHostConfig.Common: 1 V
  • PM Core:
  • PMC/Model and Model/StudyAggregate: 6 V, 2 M (Study, ExtractionInfo)
  • PMC/Interfaces: 2 V, 4 M
  • PMC/Services: 2 M (ReviewSubmissionService, plus StageReviewService, which is deleted)
  • PMC csproj: 1 M
  • PMT and PMT/Repositories: 10 V, 2 M
  • PM messages and data:
  • PM.Messages/Commands: 4 V
  • PMD and PMD/Repositories: 3 V, 3 M
  • PM endpoint:
  • Endpoint/Consumers: 4 V
  • Endpoint.Tests: 2 V
  • Endpoint/Program.cs: 1 M
  • API:
  • API/SignalR and SignalR/Dtos: 1 V, 2 M
  • API/Models: 1 V
  • API/Controllers: 2 M
  • API/Program.cs: 1 M
  • APIT and APIT/SignalR: 7 V, 3 M

1.3 How C relates to its base and to the stack

  • Commit chain. C's first-parent chain is 874a421fb → 2712901152 (B's squash) → 36190433d (A's original squash). C has exactly two non-merge commits that are on neither A nor main.
  • A's later work. A's tip af5136696 has 37 non-merge commits that C never absorbed. The known fact said about 51.
  • Status against its base. GitHub reports the PR as MERGEABLE (UNSTABLE) against its base.

1.4 Drift and conflicts with main

  • Size of the drift.
  • The merge base with main is de8e312ad (24 April), and main has 6,857 commits since.
  • 103 of C's 225 own files have changed on main since then.
  • Trial merge. A trivial git merge-tree de8e312ad origin/main 1b93cbbc4 reports:
  • 61 files changed in both, with 545 conflict blocks;
  • 29 files added in both.

The conflicting files include Study.cs, ExtractionInfo.cs, SessionTally.cs, StudyRepository.cs, ReviewController.cs, NotificationHub.cs, DataExportController.cs, CsvDataExportWriter.cs, IPmUnitOfWork.cs and MongoPmUnitOfWork.cs. - Semantic drift that git does not flag: - Validator rules (#2635, #2648, #2651). C's conversion runs A's CrossQuestionValidationService and throws on errors. A Study question with a parent fails AQ009, which #2648 now allows (PMC/Services/CrossQuestionValidationService.cs:320-328). - Embedded session fields. main's embedded AnnotationSession has six persisted fields that C's model lacks: CreatedAtUtc, CompletedAtUtc, ReservedAtUtc, FormDirtiedAtUtc, AllocationRegimeId and EntityOrder. - Bulk-update locks. main now has IAggregateWriteGuard with the bulk-update lock guard (#3909, da3edd7f6). - Statistics and exports. main has FEAT-024 statistics (ProjectStatisticsAggregate, from 2 September) and export authorisation (#3243, 6591bb90e).

2. Harvest entries

ID PR Component (type or file at SHA) What it does Verdict Why (rule IDs, decisions) Target (spec §, contract, release, tracker row) Adaptation needed Tests to carry Effort Evidence (path:line @ short SHA)
H-MIG-01 #2574 ProjectQuestionMigrationDomainService with ProjectQuestionMigrationPlan Builds an in-memory plan for one project: v1 question versions that keep the legacy question GUIDs; system questions for the project's SystemQuestionVersion; one PQS v1; one SQS v1 per stage, with system questions added to extraction stages; a question-to-version map Adapt Matches BC §3.3 "Baseline structures" (v1 per question, original IDs, one structure per stage set) and BC-R15's one-form-per-stage default. Breaks BC-R04, R05 and R09 by "repairing" orphan category questions with an invented parent. Breaks BC-R26 by throwing on authoring-rule validation instead of recording a finding, which is fatal for projects that are valid since #2648 and #2651 BC §3.3, §3.4 (question, system-question, stage-set and target rows), §4.2 dry-run preview; C4; T-BC-03 (and T-BC-01 for the preview) Emit a manifest draft and a legacy activity mapping, not aggregates. Target RD's QuestionDefinition/Version and AnnotationForm/FormVersion, with standardTarget taken from the effective legacy target and targetOneHandling = NoAcceptance (RS-R04a). Pin global system-question versions instead of copying them (H-MIG-02). Mint stable option IDs. Derive IDs deterministically from (project, manifest lineage, legacy ID) and keep legacy IDs in LegacyIdAlias. Turn repairs and validation errors into adoption findings with dispositions. Never validate legacy data against new authoring rules ProjectQuestionMigrationDomainServiceTests (13), adapted. New tests: two projects in one database; legacy-invalid structures yield findings rather than exceptions M PMC/Services/ProjectQuestionMigrationDomainService.cs:71-117, 138-144, 218-225, 246-252 @874a421
H-MIG-02 #2574 (factory from #2572) System-question v2 documents: SystemQuestionFactory output persisted into pmAnnotationQuestionV2 Writes 18 system questions per project, each with a globally hard-coded GUID as _id and a ProjectId field Avoid The defect is confirmed (§3, D-01): a second project, or a retry, hits a duplicate key and aborts. It also contradicts C4 and D2-06: system questions live in one global store, are seeded idempotently and are pinned by (guid, SystemQuestionVersion, seq), never copied per project. The factory's v0/v1 definitions stay as the §3.7 seed builder (PR-A's audit owns that entry) Versioning model §3.7; C4 system-question row; T-RD-02 Not ported. Seed the global store once, and let conversion pin versions FX-VM-25, FX-VM-26 instead — PMC/Services/SystemQuestionFactory.cs:18-46; PMC/Model/QuestionVersioning/AnnotationQuestionV2.cs:259; PMD/Repositories/AnnotationQuestionV2Repository.cs:12-31; src/libs/mongo/SyRF.Mongo.Common/MongoContext.cs:146-153 @874a421
H-MIG-03 #2574 ProjectQuestionMigrationApplicationService with MigrationReport and Project.QuestionMigrationStatus Orchestrates migration: insert question docs; for each 500-study batch, extract, check parity, persist and re-check; set the project marker; activate extracted state on every Study; refresh statistics. Returns an in-memory report Avoid Non-transactional, non-idempotent and has no caller (D-02 to D-04). Writes to legacy originals (D-05). Its dry run skips the study scan and runs with the normal read-write unit of work (D-08). It persists no inventory, manifest, attempt, parity report or quarantine record (BC §3.3). One status flag stands for both admission and ownership, against C16 BC §4.1 to §4.6; ADR-020-shaped operation of kind baselineConversion; T-BC-06, T-BC-08 Not ported. Only the batch shape is worth keeping as a note (extract, verify, persist, verify again); BC §4.4 already requires it "Persist extracted before marking migrated" and "dry run persists nothing" from ProjectQuestionMigrationApplicationServiceFlowTests (23), as cases for BC-AE01, 02 and 24 — PMA/Services/ProjectQuestionMigrationApplicationService.cs:46-161 (dry run returns at 71-74; no session or transaction anywhere) @874a421
H-MIG-04 #2574 ReviewStateMigrationDomainService.BuildExtractionBatch, using PR-A's *.CreateMigratedFromEmbedded Turns each embedded session into an extracted session version, annotations with a source session version and question version, and outcome data linked to the cohort, experiment and outcome annotation versions Adapt The shape matches BC §3.3 "Converted evidence snapshots" (one converted session version, one revision per answer, outcome rows under the schema). It breaks BC-R08 and R10 and BC-AE03 and 04: version time and author become migratedAt/migratedBy; legacy DateTimeCreated and the Annotation.Question wording are dropped; no legacy-gap state is written. Version IDs are random (Guid.NewGuid), so it is not deterministic (BC-AE02). Unresolved references throw instead of producing a disposition BC §3.2, §3.3, §3.4 (annotation sessions, answers and outcome rows); C1; C14; T-BC-03, T-BC-05 Target RD records (pmFormSession, pmFormSessionVersion, pmAnnotationHead, pmAnnotationRevision) and O1 series and observations. Use deterministic IDs. Carry the original time (UnknownLegacyTime when null), the author (UnknownLegacyAuthor), and AuthoredUnder as Verified(v1) or Unknown from the wording. Mark Completed as LegacyCompletionUnvalidated and outcome defaults as ValueOrDefaultUnknown. Convert duplicates to a Conflicted head; keep E10's membership-uncertain flag. Turn throws into manifest dispositions. Include main's six session fields The scenario builder in ProjectMigrationMongoIntegrationTests (4 annotations, 1 session, 1 outcome) as a fixture seed L PMC/Services/ReviewStateMigrationDomainService.cs:30-66, 93-97, 142-149, 176-201; PMC/Model/AnnotationAggregate/Annotation.cs:64-90 @874a421
H-MIG-05 #2574 Extracted-state activation: Study.EnableExtractedReviewState(…), ExtractionInfo.ClearReviewReadModel and the ExtractionInfo class map Sets UsesExtractedReviewState, clears the embedded Annotations, Sessions and OutcomeData, and stops serialising them and SessionTallies. The Study is then saved by full-document replace Avoid Destructive (D-05): the migration deletes the legacy embedded originals from pmStudy, against BC-R29 and BC §4.9 step 4. Removing persisted SessionTallies breaks legacy queries, and an older binary reads a migrated Study as empty, against the C16 floor. The marker is set by the migration itself, not through ADR-020's lock, verify, stamp and release C16; R0 (markers); BC §4.5 Not ported. R0's CanonicalScopes marker plus R0's reader floor replace it; legacy originals stay byte-identical (BC-AE28) — PMC/Model/StudyAggregate/Study.cs:137, 226-253; PMD/Repositories/StudyRepository.cs:2684-2697; PMT/QuestionVersioning/ProjectMigrationMongoIntegrationTests.cs:52-55 @874a421
H-MIG-06 #2574 MigrationValidationService (ValidateStudyParity, ReviewStateParityReport) Compares structural parity per record: counts; ID sets; annotation identity, answer, notes and children; session identity, status, membership and order; outcome identity and measurements; optional version anchors Adapt Already an "R6 parity check" in versioning model §12.6. It covers BC-AE13's answers and outcome-data dimensions only. It throws instead of reporting, compares a round trip rather than legacy against shadow, and has no list of expected differences explained by legacy-gap states BC §3.3 "Conversion parity report", §4.4; BC-AE13; C16 conformance; T-BC-06 Compare legacy records against shadow canonical records. Persist the result as pmConversionParityReport per (attempt, run), and quarantine instead of throwing. Classify differences explained by legacy-gap states as expected. Add the other parity dimensions: decisions and outcomes, pools (set equality), offered work, permission-filtered API output, exports, statistics (#3845) and timings. Compare author, time and wording where known MigrationValidationServiceTests (4: matching, membership mismatch, order mismatch, version anchors) M PMC/Services/MigrationValidationService.cs:32-113, 197-318 @874a421
H-MIG-07 #2574 MigratedStudyReadModelAssembler and ExtractedAnnotationLegacyMapper, as used by StudyDataService and MigratedStudyDtoAssembler Projects extracted records back into legacy embedded shapes in batches by Study. Children are ordered by question ordinal, then session order. Unchanged legacy export writers and DTO builders read from it Adapt It is the projection side of parity. Running the unchanged legacy export writers over converted data gives export parity by construction (BC-AE13). §12.6 lists it as an "R6 adapter", but it is lossy: Question is rebuilt from version text, DateTimeCreated is set to the migration time, missing references throw, and the session shape lacks main's six fields. Its rollback use is avoided (H-MIG-08). It is not the domain model's LegacyReviewDataAdapter, which reads in the opposite direction BC §4.4, BC-AE13 (exports); domain model §2 (coexistence); T-BC-06 Retarget to RD records. Carry the stored wording, legacy time and author with legacy-gap labels, and every current embedded field. Report missing references as parity findings. Keep it for parity and legacy-shaped exports only; R7 retires it ExtractedAnnotationLegacyMapperTests (3); AssembleAsync_OnMigratedStudy_ShouldNotMutateOrPersistEmbeddedReviewState M PMC/Services/MigratedStudyReadModelAssembler.cs:31-142; PMC/Services/ExtractedAnnotationLegacyMapper.cs:29-64, 137-164 (144-146, 163) @874a421
H-MIG-08 #2574 ReconstructiveRollbackService, RollbackProjectMigrationAsync, PlanRollback and Study.DisableExtractedReviewState Rebuilds the embedded state from extracted records, clears the marker and saves the Studies. Then it deletes every extracted annotation, session and outcome document and the project's question documents, and resets the PQS and SQS Avoid Destructive down-migration and hand-back to legacy (D-07): against C16's rollback bullet, BC-R27, R29, §4.9 and §4.10, AC-M0-04 and §12.6's avoid column. It flattens canonical writes made after migration into legacy records. The reconstruction is lossy: sessions keep 5 fields, and times become the migration time BC §4.9 routing rollback, which needs firstCanonicalWriteAt to be null; §4.10; T-BC-06 Not ported. BC-AE19 and BC-AE20 specify the replacement — PMC/Services/ReconstructiveRollbackService.cs:13-16, 55-80, 105-118; PMA/Services/ProjectQuestionMigrationApplicationService.cs:163-238; PMC/Services/ProjectQuestionMigrationDomainService.cs:122-130 @874a421
H-MIG-09 #2574 StudyRepository.BackfillAnnotationVersionIdsAsync and embedded Annotation.QuestionVersionId UpdateMany with array filters setting a new field inside ExtractionInfo.Annotations[] Avoid The C16 floor forbids new fields inside ExtractionInfo. It is an UpdateMany on pmStudy with no Audit.Version bump and no lock or marker check, the writer class C16 inventories. The migration never calls it (the flow test asserts this) C16 inventory; T-BC-01 (writer inventory) Not ported. Add it to the inventory only as a pattern to refuse — PMD/Repositories/StudyRepository.cs:2734-2763; PMC/Model/StudyAggregate/Annotation.cs (+QuestionVersionId) @874a421
H-API-01 #2574 QuestionManagementV2Controller (17 endpoints under api/v2/projects) and its DTOs Draft CRUD and autosave on the question aggregate; reorder; SQS draft; stage publish; stage impact; validate; version history; apply-version-as-draft; replace-with-new-version; remove-from-draft; question-set reads Reference only The endpoint inventory and the 409 shapes are useful, but they do not match RD or C4. Problems: publish is keyed by stage (PQS/SQS) not by form version; drafts live on the question and in the Project document, not RD §3.7's DesignDraft and change log; draft saves have no base check (R2a needs one); Optional and Multiple sit on the question, and AnswerOptionFilters is object? (§12.6 avoid); options have values but no optionId; BreakingChange is a bool, not C4's declared compatibility {declaredBy, declaredAt, rationale}; access is gated by QuestionMigrationStatus, not CanonicalEnrolment (C16); domain types leak into DTOs; there are no catalogued capability tests (AC-R2a item 9); replace-with-new-version uses ReplacementDraftLineagePlanner (avoid unless D2-03 keeps D38) RD §3.4 to §3.8, §4.7, §4.8, §4.13; C4; R1a (none: R1a writes through the legacy API with an import-target port); R2a (T-RD-02: drafts, history, reads); R2c (T-RD-05: impact, publish, apply-version as guided correction) Rebuild from the RD and C4 contracts. Use the list as a coverage checklist; restore creates a new draft from an old version QuestionManagementV2ControllerTests (18) as an inventory of cases (409 for migration mode, publish conflict, 400 validation) — API/Controllers/QuestionManagementV2Controller.cs:28-30, 58-91, 114-174, 381-449 (398), 694-706, 763-806, 871-876 @874a421
H-API-02 #2574 Publish preconditions: PublishRequestDto (ExpectedPublishedSqsVersionId, ExpectedImpactFingerprint), PublishConcurrencyConflictException and PublishConflictDto Refuses a publish whose base version or impact-preview fingerprint has changed, and returns 409 with the current impact summary. Also returns 409 while a transition is in progress Adapt The same idea as C4's preconditions (preview digest re-check, recorded admin choice) and D2-11's single active publication (PublicationInProgress); RD-AE16 C4 publication command; RD §4.8; D2-11; T-RD-04, T-RD-05 Key on the form head (formId, currentPublishedSeq, publicationSeq). Compute the digest over the impact preview for prior versions, categories and treatments. Return typed StaleBase and PublicationInProgress. Drop the stage-transition job Conflict cases from StagePublishApplicationServiceTests (6) and the controller publish tests S API/Controllers/QuestionManagementV2Controller.cs:114-174, 790-806, 920-930; PMC/Services/OptimisticConcurrencyExceptions.cs:5-24 @874a421
H-API-03 #2574 StagePublishApplicationService and StagePublishDomainService One Mongo transaction writes the promoted question docs, the Project document (PQS and SQS), a StageTransitionJob and every blocked Study Avoid The transaction is unbounded because it writes all blocked Studies. Definitions live in the Project document (§12.6 avoid). Phase 1 must be O(1) (C4, D2-10, FX-VM-07, RD-AE16). SP replaces stage transitions C4; RD §3.8, §4.8; T-RD-04 Not ported — PMA/Services/StagePublishApplicationService.cs:62-216 (168-193) @874a421
H-API-04 #2574 StageImpactReader (aggregation pipelines) Per question in a stage: counts of annotations, Studies, annotators and complete or incomplete sessions, plus answer distributions Reference only Feeds an impact preview, but it is keyed by stage, runs over PR-A's collections, has no draft_only category and no protected usage boundary (C8, Q-31, MS-03) C4 per-category counts; C8; FX-VM-35; T-RD-05 Write new readers for the per-form-version usage family Pipeline shapes as reference — PMD/Readers/StageImpactReader.cs:27-120 @874a421
H-API-05 #2574 MigratedReviewSubmissionService, MigratedReviewMutationPlanner, ReviewSubmissionConcurrencyConflictException and ReviewController's migrated branch Saves a session in one transaction: annotations, outcomes, the session version and the Study. It checks the client's expected session version and SQS version and returns a typed 409, mapping a duplicate key under the version filter to a conflict Adapt The valuable parts are: the evidence and Study written in one transaction (C1, CR-1, per-Study serialisation); the base-version check with a typed conflict (RD §4.3, AC-R2a item 2, C18); and the duplicate-key-as-CAS idiom. The parts to drop are: it writes the per-project pmProjectStats document inside every save (CR-2); it hard-deletes sessions, annotations and outcomes (R2a "no hard deletes"); it resolves LatestVersion at save time instead of the session's pinned version (FX-VM-05); it has no command ledger or retry receipt (E50); it routes by migration status (C16) RD §4.2 to §4.4; C1, C5, C18; T-RD-02 Rebuild on the R2a engine. Port the conflict DTO contract (expected and current IDs) as StaleBase, and the concurrency tests as cases ReviewSubmissionConcurrencyTests (5); ReviewControllerSubmitSessionTests (4) M PMC/Services/MigratedReviewSubmissionService.cs:44-203 (61-70, 166-191, 187-190), 205-263, 273-289, 320-329; API/Controllers/ReviewController.cs:77-90, 187-215 @874a421
H-API-06 #2574 GuardLegacyReviewPath, Study.UsesExtractedReviewState and ExtractionInfo.ThrowIfUsingExtractedReviewState Refuses legacy embedded writes once a project is migrated (a project check in the service) or once a Study uses extracted state (in aggregate methods) Reference only This is a precursor of R0's CanonicalScopes marker checked in legacy aggregate methods (C16). It falls short in five ways: the flag is set and cleared by migration and rollback, so admission changes ownership (C16); AddScreening, consumers, UpdateMany, bulk update and question-delete cascades are unguarded; there is no CAS on the document; the error is a plain InvalidOperationException, not a typed refusal that keeps the draft (BC §4.5 step 4); and no test asserts the refusal R0 floor item 3 (no tracker row); C16; BC §4.5; T-BC-06 Do not port. Use the guarded aggregate methods as a starting list for the R0 writer inventory (T-BC-01). Compose with main's IAggregateWriteGuard Write new refusal tests for each writer — PMC/Services/ReviewSubmissionService.cs:14-18, 46-51, 57-63, 72-79; PMC/Model/StudyAggregate/ExtractionInfo.cs:398-418; PMC/Model/StudyAggregate/Study.cs:278-293 @874a421
H-API-07 #2574 (copied from #2543) ADR-009 pieces: StudyAssignmentPolicy, StudyAssignment, ReviewStatus, IStudyAssignmentPolicy, ReviewStatsQueryService and tests; plus C's moves of StageReviewService into Application, StudyAssignmentStore, ReviewStudyQueryService and ReviewDomainService The extraction of study assignment into an Application layer Avoid Superseded: #2543 merged on 27 April (6506f7e28), and 11 of C's copies are byte-identical to it. C's move of StageReviewService into Application contradicts ADR-009, which classifies it as a Domain Service in Core. main has since moved claims to StudyAssignmentClaim (FEAT-024 slice 5) ADR-009 on main; X-CLAIMS Not ported — PMA/Services/StageReviewService.cs:9-11 @874a421 against docs/decisions/ADR-009-domain-vs-application-service-classification.md:67 @7673ed0
H-API-08 #2574 (B's job) StageTransitionWorker, StageTransitionWorkloadService, three stage-transition readers, StageTransitionJobRepository and StageTransitionBackgroundService Rebases sessions onto a new SQS after a stage publish: claims a job lease, batches the work and saves the job in the same transaction. Runs as a hosted service in the API host Avoid SP replaces stage transitions. B's job has no failure state (known fact). The hosted service is always on and has no flag decision (repository rule). ADR-020 on main is now the operation pattern; RD §3.8's publication operation replaces this SP; ADR-020; RD §3.8 Not ported — PMC/Services/StageTransitionWorker.cs:47-140; API/Services/StageTransitionBackgroundService.cs:19-38 @874a421; API/Program.cs:142 @1b93cbb
H-API-09 #2574 ProjectStatsAggregate, ProjectStatsService and ProjectStatsRepository (pmProjectStats) A materialised per-project statistics document. It is refreshed inside save transactions, and writes are refused when it is not initialised Avoid Superseded by FEAT-024 (ProjectStatisticsAggregate, ADR-019). Writing a per-project document in interactive transactions breaks CR-2. A statistics subsystem gating writes is against FEAT-024's dark, default-off rules C8; FEAT-024 Not ported — PMC/Services/ProjectStatsService.cs:21-90 (59-63) @874a421
H-API-10 #2574 Repositories and class maps for PR-A's extracted aggregates (AnnotationRepository, AnnotationSessionRepository, OutcomeDataRepository, AnnotationQuestionV2Repository, the MongoPmUnitOfWork additions) Persistence for pmAnnotation, pmAnnotationSession, pmOutcomeData and pmAnnotationQuestionV2 Avoid The storage shapes are PR-A's: unbounded version arrays inside each document, no entity path or owner scope (§12.6 avoid). RD and the storage ADR define the canonical collections (pmAnnotationHead, pmAnnotationRevision, pmFormSessionVersion) RD §3.9; versioning model §12.1; F1a storage ADR Not ported — PMD/Repositories/AnnotationRepository.cs, AnnotationSessionRepository.cs; PMC/Model/AnnotationAggregate/Annotation.cs:32 @874a421
H-API-11 #2574 ExportSpec (ExportMode, ExportVersionSelector), FromLegacyRequest/Normalise and the controller gate IsR1Supported Reserves the modes AsOfDate, AsOfQuestionSetVersion, CompareVersions and SessionAuditTrail, but accepts only CurrentState. Older request shapes stay compatible Adapt §12.6 harvest: the mode reservation becomes form-version selectors. The parts to avoid are the stage-keyed selectors (PQS/SQS scopes) and the raw AsOfDate DateTime. C11 modes are current, previous versions and as-of under the as-of rule C11; EX1, EX2; AC-R2a-05 (previous versions, T-RD-02); AC-R5a-01 to 03 (as-of; no tracker row, §6) Modes become Current, PreviousVersions (session versions) and AsOf (an HLC watermark validated by the C11 as-of rule). Selectors become (formId, seq) and session-version IDs. Keep the server refusing unsupported modes, behind flags DataExportControllerTests (4: an unsupported mode returns 400; DTO mapping) S PMC/Model/DataExportJobAggregate/ExportSpec.cs:6-19, 72-100; API/Controllers/DataExportController.cs:69-78 @874a421
H-API-12 #2574 ExportSchemaSidecar, ExportQuestionCatalog and ExportSchemaObservationReader Optional sidecar per export: each question column's ID, text, category, parent, version, system version, column kinds and the question versions observed in the data, with counts Adapt Close to RI §3.9's machine-readable codebook and C11's manifest (dataset classification, per-cell version, class and option IDs, FX-VM-13) RI §3.9 (T-RI-08); C11 manifests; AC-R2a-05 (T-RD-02) Key by question version and form version, and add class, option IDs, requiredness, answeredUnderVersion, legacy-gap coverage labels and versioned or current-only dataset labels. Rebuild the observation reader over canonical revisions CsvDataExportWriterBoundaryTests (1) M PMC/Services/DataExportServices/ExportSchemaSidecar.cs:8-48; ExportQuestionCatalog.cs:8-39; PMD/Readers/ExportSchemaObservationReader.cs:30-121 @874a421
H-API-13 #2574 DraftSnapshotService (grandfather-father-son retention) Keeps up to 50 draft snapshots per project in the Project document and prunes them by 24-hour, 7-day and 30-day tiers Avoid RD §3.7: design drafts are an append-only change log in which no change is ever edited or pruned. Drafts must not live in the Project document (§12.6) RD §3.7; T-RD-02 Not ported — PMC/Services/DraftSnapshotService.cs:8-30 @874a421

Corrections to versioning model §12.6 (this audit applies where they differ):

  • Wrong release for the "R6 adapters and parity checks". They belong to T-BC-06 (parity and cutover tooling before any production pilot) and to the staging trials (T-BC-03, T-BC-05). R6 now means universal waves (T-BC-08).
  • ExtractedAnnotationLegacyMapper and MigratedStudyReadModelAssembler are lossy as written (H-MIG-07). They may be harvested only with the wording, time and session-field fixes.
  • Add to the avoid column:
  • the extracted-state activation that erases embedded originals (H-MIG-05);
  • BackfillAnnotationVersionIdsAsync (H-MIG-09);
  • per-project system-question copies (H-MIG-02);
  • pmProjectStats writes inside save transactions (H-API-09);
  • hard deletes in the migrated save path (H-API-05);
  • the unbounded stage-publish transaction (H-API-03);
  • stage transitions (H-API-08);
  • DraftSnapshotService (H-API-13).
  • RevertToEmbeddedQuestionModel is not in #2574. It appears only in PR-A's draft ADR-012. C's equivalent is RollbackProjectMigrationAsync (H-MIG-08).

3. Defects not to carry over

ID Known fact or new Status Evidence
D-01 Known: hard-coded system-question GUIDs, so only one project can migrate Confirmed. SystemQuestionFactory creates AnnotationQuestionV2(def.Id) with the global AQ.*Guid constants. The migration upserts them into the single pmAnnotationQuestionV2 collection. The upsert filter is {_id, Audit.Version ∈ {0, missing}}, so a second project's insert, or a retry by the same project, misses the stored version-1 document and hits E11000 on _id. The ordered bulk write aborts the whole migration at step 1. No integration test migrates two projects SystemQuestionFactory.cs:18-46, AnnotationQuestionV2.cs:259, ProjectQuestionMigrationApplicationService.cs:76, MongoUnitOfWorkBase.cs:355-372, MongoExtensions.cs:462-476 @874a421
D-02 Known: not transactional Confirmed. MigrateProjectAsync never starts a session. It writes in four separate phases: question documents, extracted batches, the project marker, then Study activation ProjectQuestionMigrationApplicationService.cs:76-158
D-03 Known: not idempotent Confirmed. A crash before the project marker makes every retry fail with a duplicate key (D-01). A crash after the marker short-circuits retries with AlreadyMigrated (:54-66), so the remaining Studies are never activated. Session version IDs (Guid.NewGuid) and the PQS and SQS IDs change on every run ProjectQuestionMigrationApplicationService.cs:54-66; ReviewStateMigrationDomainService.cs:149
D-04 Known: no caller Confirmed. At 1b93cbb, nothing outside the tests references IProjectQuestionMigrationApplicationService: no endpoint, consumer, job or DI registration git grep over src
D-05 New: migration deletes the legacy originals Activation clears the embedded Annotations, Sessions and OutcomeData. The class map stops serialising them and SessionTallies once UsesExtractedReviewState is set, and a full-document replace then removes them from pmStudy. The integration test asserts that they are empty. This breaks BC-R29, BC §4.9 step 4 and the C16 floor: an older image reads a migrated Study as having no review data Study.cs:226-253; StudyRepository.cs:2684-2697; ProjectMigrationMongoIntegrationTests.cs:52-55
D-06 New: legacy timestamps, wording and session fields are lost Converted versions take migratedBy and migratedAt. AnnotationMutationMapper carries neither DateTimeCreated nor Annotation.Question. The mapper rebuilds Question from the version text and DateTimeCreated from the version's CreatedAt. C's embedded session has only 4 fields, while main's has 10. Combined with D-05, the loss is permanent. This breaks BC-R08, R10, AE03 and AE04 and versioning model §11 (AuthoredUnder) Annotation.cs:64-90; ExtractedAnnotationLegacyMapper.cs:144-146, 163; AnnotationSession.cs:26-52 @1b93cbb
D-07 New: rollback is destructive and hands the scope back It rebuilds embedded state, clears the marker and deletes the extracted documents and the project's question documents. Any work saved after migration through MigratedReviewSubmissionService is flattened into legacy records, against C16, BC-R27 and AC-M0-04 ReconstructiveRollbackService.cs:55-80; ProjectQuestionMigrationApplicationService.cs:206-233
D-08 New: the dry run is not a BC dry run dryRun returns before any Study is read. It runs with the read-write unit of work, and it produces no counts, checksums or Q-35 premise count (BC §4.2, BC-AE01) ProjectQuestionMigrationApplicationService.cs:68-74
D-09 New: conversion invents structure and refuses projects that are valid on main "Repairs" set an invented parent on orphan category questions. Authoring validation (AQ009, and parentQuestion.System for anchors) throws, so projects valid since #2648 and #2651 fail to convert, with no quarantine record (BC-R04, R09, R26) ProjectQuestionMigrationDomainService.cs:138-144, 246-252; CrossQuestionValidationService.cs:316-328
D-10 New: the migrated save path breaks engine rules It refreshes the per-project pmProjectStats inside each save transaction (CR-2). DeleteSessionAsync hard-deletes sessions, annotations and outcomes. It resolves q.LatestVersion at save time instead of the pinned version. EnsureProjectWritableAsync refuses saves when statistics are not initialised MigratedReviewSubmissionService.cs:187-190, 239-255, 283-288; ProjectStatsService.cs:59-63
D-11 New: unbounded publish transaction, and no flag decisions Stage publish writes every blocked Study in one transaction. StageTransitionBackgroundService runs unconditionally in the API host. No QM v2 surface records a flag decision, as the repository rules require StagePublishApplicationService.cs:168-193; Program.cs:142 @1b93cbb
D-12 New: the PR body misattributes files The five assignment-state files listed as #2467 types are QM's own (#2461 3562ff913). This matters for the conflict surface: they are not superseded by #2467's merge git log 0d943a34d^2 -- …/StudyAggregate/ReviewerAssignmentState.cs returns nothing

4. Superseded by main since the PR was written

  • #2467 merged on 30 August 2026 (0d943a34d), with SlotReservation, ReviewerPresence and ReviewSessionConnection. All 73 embedded #2467 files are superseded. main already has the IUpdateBuilder/MongoUpdateBuilder infrastructure, the PM idle, suspension and liveness consumers, and StudyReviewPresenceSnapshot.
  • #2543 merged on 27 April 2026 (6506f7e28), with ADR-009 Approved and Implemented. C's 22 copies are superseded, and its move of StageReviewService into Application contradicts the ADR.
  • FEAT-024 materialised statistics (ProjectStatisticsAggregate from 78c3caf48, 2 September; ADR-019; the claim fold StudyAssignmentClaim, a5540ee71) supersede pmProjectStats.
  • Bulk-update locks on every Study write path (#3909, da3edd7f6): main's IAggregateWriteGuard is the seam that R0's ownership guard composes with, which supersedes GuardLegacyReviewPath as a pattern.
  • ADR-020 (bulk study update, all or nothing) on main is the operation pattern that conversion cutover reuses (BC §3.3). It supersedes the stage-transition job pattern.
  • Export: #3243 export authorisation (6591bb90e), formula neutralisation (412a1bbb0) and comments in the quantitative export change the same writers and controller that C changes.
  • Validators: #2635 (overlapping IDs tolerated), #2648 (nested Study parents allowed) and #2651 (anchor system questions split) change what C's conversion validation would refuse.
  • Owner session (5 October 2026): BC's universal faithful conversion, firstCanonicalWriteAt routing rollback and quarantine replace C's model of per-project migration plus reconstructive rollback. RD's DesignDraft, form versions with standardTarget and generated session versions replace PQS/SQS stage publishing.

5. Closure-note draft

#2574 (QM v2 PR-C), harvested. This PR's migration, cutover and API work was audited against the October 2026 specifications (harvest map entries H-MIG-01 to H-MIG-09 and H-API-01 to H-API-13). The conversion planner, review-state extraction, parity checker, canonical-to-legacy projection and export mode reservation carry forward as adapted designs and test cases. They go into the baseline-conversion rows T-BC-03, T-BC-05 and T-BC-06, the R2a and R2c rows T-RD-02, T-RD-04 and T-RD-05, and the codebook row T-RI-08. The reconstructive rollback, the erasure of embedded state, per-project system-question copies, the stage-publish transaction and stage transitions are deliberately not carried over. The embedded #2467 and #2543 files are superseded by those PRs' own merges into main.

6. Proposed tracker rows

Everything attaches to existing rows. No new row is needed except possibly one (last bullet).

  • T-BC-01 (inventory and dry run):
  • Add C's writers to the writer inventory as examples to refuse: the UpdateMany in BackfillAnnotationVersionIdsAsync (H-MIG-09) and the full-replace save after activation (H-MIG-05).
  • Add the H-MIG-01 plan builder as the starting point for the dry-run mapping preview.
  • Add two fixtures: two projects in one database, and a project valid since #2648.
  • Acceptance: BC-AE01 and BC-AE02.
  • T-BC-03 (annotation scope): H-MIG-01 and H-MIG-04, with the fixture seed from ProjectMigrationMongoIntegrationTests. Acceptance: BC-AE03, 04, 06 and 09.
  • T-BC-05 (extraction): the outcome part of H-MIG-04, with links to the cohort, experiment and outcome annotations. Acceptance: BC-AE05.
  • T-BC-06 (parity, cutover, rollback):
  • Carry H-MIG-06 and H-MIG-07 and their tests.
  • Add a regression fixture: a crash at each migration phase, then a retry (BC-AE18, BC-AE24).
  • Record H-MIG-08 as the example of what routing rollback must not do (BC-AE19, BC-AE20).
  • T-RD-02 (R2a):
  • The H-API-05 conflict contract and concurrency tests (AC-R2a item 2).
  • H-API-01 as a coverage checklist for drafts, history and reads.
  • The previous-versions part of H-API-11 and H-API-12 (AC-R2a-05).
  • T-RD-04 and T-RD-05 (F2 and R2c): H-API-02, the preview-digest and publication-in-progress preconditions (RD-AE16), and H-API-04 as reference for the impact counts.
  • T-RI-08 (codebook): H-API-12.
  • Gap. R5a as-of exports (AC-R5a-01 to 03, C11) have no tracker row. If the programme lead wants one, the proposal is:
  • ID: T-RI-11;
  • scope: as-of and manifest export modes on the adapted ExportSpec;
  • release: R5a;
  • depends on: T-RD-02 and the F6a as-of rule;
  • acceptance: AC-R5a-01 to 03 and C11-T02 and T04.

7. Owner-level questions

None.