Skip to content

AF2 Phase 3.4 Implementation Plan

Goal

Keep the dormant Accelerated Annotation Form v2 responsive and accessible for large non-experiment studies by bounding the mounted unit and question DOM, while adding the read-only study source context required beside the form.

Outcome

AF2 exposes a collapsible source sidebar with title, authors, publication, year, abstract, and a secure PDF link. Only a bounded number of unit forms and question controls are mounted at once. Keyboard users and assistive technology can navigate panels, controls, validation messages, and first-error movement even when the target content must first be expanded, paged, or materialized.

The form remains dormant and off-route. P3.5 owns StageReview activation and the browser performance gate.

Dependencies and boundaries

  • Track 0, Phase 1, Phase 2A/2B, and P3.1-P3.3 are merged on main.
  • P3.3 provides unit paging, multi-open workspace state, fullscreen editing, stable draft identities, and first-error navigation.
  • P3.5 depends on this PR. The quote-into-comment fast-follow depends on P3.5.
  • No backend endpoint, payload, migration, Question Management v2 behavior, route host, Experiment/outcome-data surface, feature flag value, environment, deployment, secret, reconcile host, or preview host changes are in scope.
  • Source refresh must never replace or reset the editable annotation snapshot.
  • Whole-session save/complete remains the only persistence boundary.

Architecture decisions

Separate source metadata channel

Add AnnotationFormStudySource and a read-only studySource signal to AnnotationFormDataSource. V1AnnotationFormDataSource maps it from the current study independently of AnnotationFormSnapshot; source-only emissions must not call store.hydrate() or erase dirty drafts. The UI renders plain text and an ordinary secure PDF anchor. It does not reuse the legacy PDF loader, window.open, quoting, or prototype AI behavior.

Bounded unit forms

The selector's existing page size remains ten. openAll() may preserve logical open state across the category, but the workspace only instantiates panels for the current bounded unit page. Each panel owns its deferred AnnotationUnitFormComponent; projected form content cannot be used because Angular would instantiate it before the panel's defer boundary.

Logical open state per unit is owned by AnnotationFormV2Store alongside draft identity and touched-field state. The displayed open count is therefore the logically open category count, not merely the currently mounted page subset.

Closing, paging, or switching categories destroys the form view only. Drafts, touched state derived by the store, and stable IDs remain authoritative outside the view and reappear unchanged when remounted.

Bounded question rendering

Replace recursive template traversal with a pure, stack-safe preorder projection of visible question occurrences. The projection preserves question order, child order, repeated occurrence identity, label-root elision, depth, filtered options, and finite handling of malformed cycles.

Use two explicit render paths:

  1. Structurally compact scalar rows use a fixed-size CDK virtual viewport with reserved help/error space, a minimal template cache, and no append-only DOM.
  2. Complex rows (children, descriptions, notes, repeated/array answers, or measured overflow) use a bounded natural-height window with native Previous and Next controls.

The fixed-height admission gate rejects rows with children, descriptions, notes, repeated answers, array answers, or variable option lists such as radio and checklist controls. If any visible row is rejected, the whole ordered sequence uses bounded natural pages so mixed row types cannot reorder. A measured overflow is the final runtime disqualifier.

No variable-height question is forced into an unsafe fixed-size contract. A runtime overflow guard moves a row to the bounded natural-height path rather than clipping content.

Focus and accessibility contract

First-error focus becomes an asynchronous reveal pipeline:

  1. select the target category and unit page;
  2. open and materialize the owning panel;
  3. reveal the target virtual row or bounded page;
  4. mark the field touched;
  5. focus the actual namespaced control and announce the error.

Each asynchronous reveal boundary is cancellable and has a two-second timeout. If the current request cannot materialize or focus its target, focus returns to the owning unit card and a live-region message directs the user to the bounded unit/question navigation instead of leaving the command pending indefinitely.

Panel toggles expose aria-controls and accurate expanded state. Unit groups use fieldset/legend. Every control connects its help and error text through aria-describedby, exposes aria-invalid, and keeps a question-specific accessible name for notes/comments. Focus returns predictably when a focused unit is closed, deleted, hidden, or removed from the current page. Native button, tab, input, radio, checkbox, and Material keyboard behavior is retained instead of adding competing custom shortcuts. The virtual viewport exposes a native continuation button after its mounted rows so forward Tab traversal can materialize and focus the next unmounted question range.

Delivery tasks

Task 1 — Source model, adapter, and sidebar

  • Add a nullable AnnotationFormStudySource model and separate data-source signal with stable defaults for isolated tests.
  • Map title, authors, publication, year, abstract, and PDF URL from the current v1 study. Preserve the established publication-name fallback and newline rendering conventions.
  • Add a standalone collapsible source-sidebar component with semantic headings, labelled toggle, missing-field behavior, and safe PDF anchor.
  • Integrate it into the AF2 shell without changing snapshot hydration.
  • Prove source-only updates do not reset dirty drafts or the active session.

Task 2 — Pure question occurrence projection

  • Extract a stack-safe, deterministic flat-row projection from QuestionListComponent.
  • Preserve label-root elision, occurrence-specific parent answers, option filtering, depth, repeated rows, child ordering, and cycle finiteness.
  • Classify rows as fixed-size-safe or complex using explicit structural criteria, with unit tests for every disqualifier.
  • Cover approximately 1,000 roots, one root with many descendants, repeated branches, missing parents, and malformed cycles without recursion limits.

Task 3 — Bounded question renderers

  • Add the CDK scrolling dependency/import already available in the Angular workspace and render fixed-size-safe rows in a virtual viewport.
  • Render complex rows through a bounded natural-height page/window.
  • Keep live form fields keyed by stable draft ID across recycling and remounts; do not duplicate namespaced DOM IDs in fullscreen mode.
  • Add an overflow fallback and tests showing neither path grows the mounted question DOM with total question count.
  • Preserve empty-list, conditional-label, and nested-depth presentation.

Task 4 — Deferred, page-bounded unit workspace

  • Make UnitPanelComponent own and defer creation of its unit form body.
  • Derive workspace panels from current-page units while retaining logical open state and accurate open counts.
  • Ensure close, page, category, and delete transitions destroy obsolete form views and recover focus to a deterministic surviving control.
  • Prove at most the selector page size of unit forms is mounted after open-all, page changes, category changes, and fullscreen activity.

Task 5 — Keyboard, ARIA, and error reveal

  • Connect help and error IDs, aria-describedby, and aria-invalid for all six control types and their repeated-answer occurrences.
  • Add semantic unit grouping and toggle/control relationships.
  • Give notes/comment fields question-specific accessible names.
  • Extend first-error navigation across deferred panels, unit pages, virtual ranges, and complex-row pages, awaiting each reveal boundary before focus.
  • Add live-region messaging without nested or duplicate alert semantics.
  • Cover focus recovery when the focused unit or row disappears.

Task 6 — Validation and handoff

  • Run the complete focused AF2 suite, including projection, DOM-bound, source-isolation, keyboard, ARIA, focus, and lifecycle regressions.
  • Run the full Angular coverage suite.
  • Run the production build.
  • Run generated-file, documentation, diff, current-main compatibility, and dormant-scope/flag/backend boundary checks.
  • Obtain independent implementation, performance, accessibility, and test review; resolve every actionable finding with regression coverage.
  • Pass normal PR CI and review gates, then merge with the guarded ship-pr workflow and clean only this isolated worktree.

Validation matrix

Risk Required proof
Source refresh overwrites edits Source-only emission changes sidebar while dirty drafts and session identity remain unchanged
Logical open-all mounts every form 200 logical open units still mount no more than the current unit page size
Virtualization clips variable rows Notes, descriptions, children, repeats, arrays, and overflow use bounded natural-height rendering
Recycling corrupts answers Edit, scroll away, return, save, and serialize by stable draft ID
Deep or cyclic graph hangs Stack-safe deterministic projection remains finite for deep and malformed graphs
Error target is not mounted Complete selects page/category, expands panel, reveals row, touches, focuses, and announces
Fullscreen duplicates IDs Workspace and every dialog keep distinct inherited control-ID namespaces
Assistive text is detached Each of six controls exposes accessible name, description, invalid state, and one error announcement
Focus disappears after mutation Close/delete/hide/page/category transitions move focus to the nearest deterministic owner
DOM grows with data size Mounted unit forms and question controls remain capped for 1,000 questions and 200 units

Validation evidence

  • Focused AF2 suite: 18 files and 233 tests passed after the final changes.
  • Production Angular build and validate:generated passed.
  • Documentation validation passed with the pre-existing unrelated warning for docs/how-to/local-mongodb-development.md ticket #2380.
  • Diff checks passed; the branch is based on current origin/main, and changed paths are limited to this plan, the dormant AF2 component subtree, and the required AF2 architecture update in CLAUDE.md.
  • The complete Angular coverage run passed 1,731 of 1,738 tests. Its seven failures were confined to the unchanged environment.effects.spec.ts after another suite removed window.location.hostname; that spec passed all 23 tests in isolation, and every AF2 test passed in the combined run. Normal PR CI remains the authoritative full-suite gate.
  • Independent implementation, performance, accessibility, and test reviews were completed. Findings around fullscreen mount bounds, direct virtual recycling proof, overflow position, touched-state remounts, bounded reveal cancellation, progress-specific announcements, virtual continuation, nested hierarchy presentation, targeted ARIA updates, stable sidebar IDs, and shared conditional visibility were resolved with regression coverage; the final focused re-audit reported no findings.

Rollback

AF2 remains unreferenced by an active route, so rollback is a normal revert of this PR. No data, backend, flag, deployment, or environment rollback is needed.