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:
- 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.
- 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:
- select the target category and unit page;
- open and materialize the owning panel;
- reveal the target virtual row or bounded page;
- mark the field touched;
- 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
AnnotationFormStudySourcemodel 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
UnitPanelComponentown 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, andaria-invalidfor 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-prworkflow 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:generatedpassed. - Documentation validation passed with the pre-existing unrelated warning for
docs/how-to/local-mongodb-development.mdticket#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 inCLAUDE.md. - The complete Angular coverage run passed 1,731 of 1,738 tests. Its seven
failures were confined to the unchanged
environment.effects.spec.tsafter another suite removedwindow.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.