Skip to content

AF2 Phase 3.5 Implementation Plan

Goal

Make the Accelerated Annotation Form v2 user-visible on the existing ordinary stage-review route for the deliberately narrow, non-experiment case, while retaining a fail-closed v1 fallback and proving that AF2 meets the Track 0 performance budget at the intended large-study shape.

Outcome

When the generated annotationFormV2 flag is enabled, an ordinary loaded stage whose selected annotation questions are all supported non-experiment questions renders exactly one AF2 host. The same route continues to render v1 when the flag is off, eligibility is unresolved, the stage is extraction or Experiment-bearing, or the host is reconcile/preview.

Stage navigation observes one save contract across both forms: v1 keeps its current observable save lifecycle, while AF2 maps only a confirmed saved result to navigation success and fails closed for invalid, cancelled, failed, rejected, or missing child states. No flag value is enabled in committed configuration.

The browser benchmark covers both approximately 1,000 supported questions and a separate 200-unit pagination workload. It proves first interaction, edit processing, category and page switching, bounded DOM, and heap plateau gates. The measured AF2 results and deltas are appended to the Track 0 baseline.

Dependencies and boundaries

  • Track 0, Phase 1, Phase 2A/2B, and P3.1-P3.4 are merged on main.
  • P3.4 supplies bounded rendering, source context, focus management, and the complete dormant AF2 shell; this PR is the first active route host.
  • The current v1 backend contracts and whole-session persistence boundary stay unchanged. No dormant Question Management v2 backend work is required.
  • Quote-into-comment starts only after this PR merges. It is an independent frontend fast-follow and does not broaden P3.5.
  • Experiment/outcome-data parity and reconcile/preview host migration remain Phase 4. App-wide zoneless change detection remains Phase 5.
  • Do not add endpoints, migrations, data changes, feature-flag activation, environment changes, secrets, deployment work, or workflow suppression.

Eligibility contract

Implement eligibility as a pure, exhaustively tested decision. AF2 is selected only when every condition is known and true:

  1. the generated global selectAnnotationFormV2 value is true;
  2. the host is ordinary stage review, not reconciliation or preview;
  3. the current project details and an annotation-capable stage are loaded and match the route context;
  4. stage.extraction is strictly false;
  5. every selected stage.annotationQuestions identifier belongs to the current project and resolves to a question entity; and
  6. every selected question category is one of Study, Hidden, Cohort, Disease Model Induction, Treatment, or Outcome Assessment;
  7. each selected mutable-unit category has exactly one visible scalar string label question; and
  8. every selected structural child has its complete selected parent chain. Ordinary roots must have no target. The two legacy system control questions whose V1 DTOs are marked as roots despite being children are normalized only when their exact IDs, categories, control shapes, targets, and system-label parents match the built-in Disease Model Induction or Treatment convention; every other root/target contradiction selects v1.

An Experiment category, an unknown category, a missing selected question, a project mismatch, an incomplete mutable-unit schema, or any loading/unresolved state selects v1. Eligibility uses the selected stage-question set rather than requiring every raw descendant in subquestionIds; unselected sibling descendants must not disqualify an otherwise valid stage.

Host and navigation contract

  • Render v1 or AF2 through one mutually exclusive branch in StageReview; do not instantiate both forms or let both subscribe to route/store state.
  • Feed AF2 validity and dirty outputs through the host's existing subjects so parent navigation and unsaved-change behavior remain compatible.
  • Keep v1 onSave(0) and sessionIsSaving true-to-false completion behavior unchanged.
  • Call AF2 saveForNavigation() for the v2 child. Return true only for saved; return false for invalid, cancelled, failed, a rejected promise, or an absent/ambiguous child.
  • Keep reconcile and preview callers on v1 in this phase. A structural guard interface may remove their dependence on the concrete v1 component, but it must preserve fail-closed navigation semantics.
  • Cover flag transitions and asynchronous loading without a transient dual host or accidental v2 selection.

Performance gate

Extend the existing isolated Playwright annotation benchmark without changing the committed default-off flag:

  • intercept the runtime appConfig.env.json response inside the benchmark and set only its in-test annotationFormV2 value to true;
  • seed an ordinary review stage with approximately 1,000 non-extraction Study/Cohort textbox questions, plus a separate ordinary-stage case with 200 supported units and a representative per-unit question graph;
  • keep Angular Signal Forms scoped to currently mounted answer rows while the form store retains off-screen drafts, so the 1,000-question workload does not perform form-graph work for every off-screen question on each edit;
  • require the first interactive AF2 category in under 1.5 seconds, combining the browser-clock host-mount-to-painted-enabled-Open all segment with its click-to-painted-usable-control segment; this retains blocked main-thread and rendering work while excluding Playwright protocol/actionability time between readiness and the synthetic click; retain route-to-host and route-to-control timings as cold navigation diagnostics rather than attributing API/app startup to AF2;
  • require application edit-to-microtask-settle p95 under 16 ms, keeping any request-animation-frame paint sample diagnostic rather than mislabelling it as the hard application-processing gate;
  • require a visible category switch in under 250 ms;
  • assert bounded mounted controls/units and a forced-GC heap plateau rather than monotonic subscription or memory growth; and
  • preserve the v1 Angular 22 baseline as the before row, then document AF2 measurements and deltas in docs/planning/annotation-form-perf-baseline.md.

The PR receives the repository's run:e2e-full label so the normal isolated E2E gate exercises this benchmark. No workflow file is changed or automated run interrupted.

Delivery tasks

1. Pure selection and host wiring

  • Add the fail-closed eligibility model and exhaustive truth-table tests.
  • Select from generated flag state plus loaded project/stage/question entities; avoid legacy feature-toggle services and loading-time empty selector ambiguity.
  • Add the mutually exclusive v1/v2 StageReview host and keep AF2 out of reconcile and preview.
  • Prove unsupported, unresolved, extraction, Experiment, and flag-off cases remain v1, including selected-question/subquestion edge cases.

2. Save and navigation compatibility

  • Forward AF2 validity/dirty state through the existing host contract.
  • Adapt AF2 promise results to the existing observable navigation guard, failing closed for every non-saved or exceptional result.
  • Preserve v1 save timing and add meaningful StageReview/guard tests that execute both child paths rather than passing through unrelated PDF setup.

3. User-visible workflow and benchmark

  • Document the supported flagged stage-review workflow in user-guide/annotating.md and the explicit v1 fallback/host contract in the developer documentation.
  • Add runtime-only flagged 1,000-question and 200-unit browser workloads, bounded DOM assertions, and repeatable heap plateau samples.
  • Record AF2 measurements and deltas beside the Track 0 baseline.

4. Validation and shipping

  • Run focused eligibility, StageReview, guard, AF2, and benchmark tests.
  • Run the complete Angular unit suite.
  • Run the production build, formatting, documentation, E2E typecheck, and generated-code checks appropriate to the changed surface.
  • Obtain an independent patch review and resolve every actionable finding.
  • Run normal PR CI, Sonar, review-thread, and run:e2e-full gates; merge through the guarded ship-pr workflow and clean the isolated worktree.

Validation evidence (2026-08-10): the exact-head Angular CI check passed. Two local full-suite runs exposed only pre-existing concurrency-sensitive SignalR and effects failures; their focused suites then passed 48/48 and 10/10. The normal full-E2E execution passed all 18 Playwright scenarios and attached the AF2 measurements recorded in the Track 0 baseline. Its later repeated-teardown failure was fixed by the separate shared-infrastructure PR #2722, now merged, so the exact-head full-E2E shipping gate can be rerun cleanly.

Post-merge focused validation added reciprocal selected-question graph checks, fail-closed loading/flag/reconciliation transitions, and recoverable user-facing persistence errors. The eligibility suite passed 65/65, the AF2 component suite passed 34/34 before the final revert-error case was added, and that new case passed 1/1 in isolation. The StageReview navigation and guard suites passed 19/19, and both web and E2E TypeScript checks passed. Normal exact-head CI and full E2E remain the final shipping evidence.

Rollback

The generated feature flag remains default-off, and every uncertain or unsupported case stays on v1. Rollback is therefore a normal revert of this host-selection PR or a separately authorized flag disablement; there is no backend, data, migration, or deployment rollback.