Skip to content

AF2 Phase 4 — Experiment/Outcome-Data Parity + Reconciliation/Preview Hosts

For agentic workers: each PR below is its own scoped task with its own review gate; work only in that PR's worktree. This plan is authoritative for Phase 4, and the Key terrain facts section below is the evidence record — its file:line citations are the inventory, to be re-verified against the code rather than against any external audit.

Goal: AF2 handles Experiment questions and outcome data (experiment×cohort×outcome×graph cells with timepoints) at full v1 parity on stage-review, and the reconciliation and admin-preview hosts migrate to AF2 — all preserving the fail-closed host selection and v1 fallback until each surface is proven.

Locked decisions (project owner, 2026-08-10)

# Decision
D-P4-1 graph2data is excluded from parity scope. It is already dead in v1 production (@wildfalcon/graph2data absent from package.json, polyfill import commented out at polyfills.ts:55, flag default-off). Documented as a pre-existing gap for a separate revive-or-remove decision — not a Phase 4 regression target.
D-P4-2 Both host migrations ship a carve-out first cut: preview (PR 7) and reconcile (PR 8) migrate for non-extraction stages as soon as ready, in parallel with the parity PRs; extraction stages keep v1 until Experiment parity (PR 5) lands — the proven Phase-3 fail-closed pattern.
D-P4-3 Outcome data is a separate outcomeDrafts array — a state slice in AnnotationFormV2Store, not a MongoDB collection (AnnotationFormOutcomeDraft[] keyed by experimentId/cohortId/outcomeId/graphId
D-P4-4 Timepoint controls are rewritten natively in Signal Forms (no CVA seam inside AF2), and Handsontable upgrades to the most recent version as a prerequisite PR (the upgrade touches v1's still-live surfaces, so it is isolated and verified against v1 before any AF2 use).
D-P4-5 The reconcile host gets a new session-scoped AnnotationFormDataSource implementation (explicit session/annotations/outcomeData/extraction inputs) rather than widening V1AnnotationFormDataSource's "current"-selector coupling — keeps stage-review's implementation untouched; matches the DI-token-swap pattern.
D-P4-6 v1's experiment component chain is rebuilt, not ported — it carries the circular-import chain (annotation-experiment-question extends annotation-question → … → annotation-experiment-question), direct parent-component injection, and FormArray path-walking that AF2's model exists to eliminate. Only the two timepoint controls are re-implementations of proven UX.

Key terrain facts (evidence record)

  • AF2's wire contract is already outcome-shaped: IEmittedAnnotations.outcomeData/hasExtraction exist and toV1StudyOutgoingDto maps them — but annotation-form-submission.ts:371-376 hardcodes hasExtraction: false, outcomeData: []. The producer is the gap, not the wire.
  • 'Experiment' is pre-wired scaffolding in AF2 (category defined, store visibleCategoryOrder, validation categoryOrder, mapV1Units emits Experiment units) — Phase 3 deliberately filtered it via ANNOTATION_FORM_PHASE_3_CATEGORIES and the eligibility guards (supportedCategories omits Experiment; stage.extraction !== false hard-fails).
  • v1's outcome model is a 4-level nested FormGroup (experiment→cohort→outcome→graph|'null') built by AnnotationFormService.initOutcomeData (annotation-form.service.ts:272-326) and serialized by convertFormToOutcomeData (:833-881) — the parity oracle for PR 3's round-trip tests.
  • v1 preview already renders extraction stages (stage-preview.component.html:63-79, dataExtraction bound to the stage) — hence D-P4-2's carve-out rather than a hard dependency.
  • Reconcile mounts N read-only form instances (one per candidate session); AF2's per-instance store isolation is fine, but V1AnnotationFormDataSource reads only "current" selectors — hence D-P4-5.
  • The graph SELECTOR is live in v1 production even though graph2data is not (verified for PR 6). v1 gates the whole graph column and the "Extract Experiment Graphs" button on FeatureToggleService.graph2Data$, which is config.graph2data || (projectSchemaVersion ?? 0) > 0 (feature-toggle.service.ts:44-46) — so every schema-version>0 project sees it with the flag off. D-P4-1's exclusion is therefore exactly the digitiser (Graph2DataComponent, opened from onGraphClick), not region-to-outcome assignment, which is in PR 6's scope.
  • v1's graph assignment is an ordinary annotation, not bespoke state: the outcome's outcomePdfGraphs answer (a hidden answerArray subquestion of the Outcome Assessment label, AnnotationQuestion.cs:529-548) holds the assigned pdfReferences annotation IDs, and GraphSelectorComponent._getPdfGraphControlForOutcome writes exactly that control. AF2 reproduces the assignment by writing the same draft — no new persistence path.
  • Marking regions on the PDF (v1's GraphSelectorComponent canvas) belongs to the pdf-tools subsystem, whose PdfLoaderService resolves its document from the global getCurrentStudy selector — the coupling the AF2 data-source seam removes and which the reconcile/preview hosts cannot satisfy. Migrating it is separate, unscheduled work; see the PR 6 note.

PR decomposition (9 PRs; review gate each; stack/parallelize as marked)

PR Scope Depends on Notes
1 Outcome-data draft model (dormant)AnnotationFormOutcomeDraft array in models/snapshot/store; data-source mapping from IStudyExtractionCharacteristicsVm + IOutcomeData[] This PR; carries this plan doc
2 Serializer + persistence wiring (dormant) — populate hasExtraction/outcomeData from outcomeDrafts; round-trip parity tests against convertFormToOutcomeData 1
3 Handsontable upgradehandsontable + @handsontable/angular to latest; verify every v1 usage (outcome-table-info grid, timepoint-spreadsheet) still works; own perf/behavior smoke — (parallel with 1–2) Touches live v1 surfaces — isolated by design (D-P4-4)
4 AF2 outcome UI (dormant, off-route) — outcome summary table, native Signal Forms timepoint entry (array + spreadsheet on upgraded Handsontable), cell editor replacing data-extraction-form dialog 1, 2, 3 Rebuilds, not ports (D-P4-6); excludes graph work
5 Eligibility opens Experiment + extraction stages (stage-review only) — flip supportedCategories/stage.extraction guards; Experiment unit CRUD (containment: Experiment contains Cohort); perf-gate re-run 1–4 Done (PR #2745): both guards are now host-conditional (supportedCategoriesFor / extractionCarveOutHosts), Experiment is a mutable-unit category, unit deletion prunes the outcome cells keyed by that unit, and the category tab + outcome matrix mount on stage-review. Preview keeps both carve-outs until PR 9. Perf gate re-run and recorded in the perf baseline.
6 Graph-selector parity — PDF-region → outcome graph assignment rebuilt on the outcomeDrafts model; excludes graph2data (D-P4-1) 1–4 (parallel with 5) Done (PR #2746), delivered as assignment-only: the Experiment unit gains a graph panel that assigns/unassigns the study's existing PDF regions (writing v1's outcomePdfGraphs answer, keeping v1's one-outcome-per-region rule), and graph cells follow that assignment as an invariant. Two gaps, both deliberate: marking/moving/deleting regions still needs v1 (it needs the pdf-tools PDF canvas — see terrain facts), and graph identity renders as "Graph N (page M)" plus a page-position minimap rather than v1's cropped bitmap, which needs the same subsystem.
7 Preview host migration (carve-out) — study-less data-source/persistence pair (empty drafts, no-op persistence), 'preview' eligibility branch; extraction-stage previews stay v1 until PR 5 1 (not 5) D-P4-2
8 Reconcile host migration (carve-out) — session-scoped data source (D-P4-5), AF2 read-only mode, reconciliation summary view replacing the Handsontable outcome-table-info grid; extraction-stage pools stay v1 until PR 5 1, 3 (not 5) D-P4-2
9 Activation + cleanup — extraction/reconcile/preview carve-outs lifted where parity is proven; delete the v1 experiment chain (finally resolving the circular-import TODO) once both hosts are fully migrated 5–8

Parallel lanes: (1→2), (3), (7), (8 after 1+3) can all proceed concurrently; (4) joins after its three dependencies; (5)/(6) after 4.

Standing gates (every PR)

Fail-closed host selection and v1 fallback preserved until each surface is explicitly proven; annotationFormV2 remains default-off pending authorized rollout; tests in-PR (exact-path --include for shared/annotation specs); production build green; the Phase-3 perf gate re-runs on any PR that changes mounted-row rendering (PRs 4, 5, 8); docs updated in-PR; no coupling to the dormant QM v2 backend.

The PRISMA Phase 2 specifications in docs/features/prisma-specification/ remain binding constraints on every PR in this plan — outcome data and extraction are defined there, so any change to what an outcome-data cell means, how extraction entities relate, or how they are counted must comply with those specs (see in particular three-level-data-model.md and prisma-constraint-annotations.md).