Skip to content

AF2 and stage-review remaining delivery

The minimum usable release completes the existing branch tabs, restores AF1 action/category behaviour, and corrects controls and navigation in the redesigned workspace. It preserves saved answers, incomplete drafts, permissions, and the legacy rollback path. Personalisation follows in independently shipped slices; it remains required programme scope.

Sequence and observable acceptance

Slice Dependencies Acceptance and evidence
Branch tabs, #2810 Merged isolation repair #3207 Current-head AF2/stage-review tests, review and CI pass; switching tabs preserves drafts and control identity; nested errors reveal ancestors; keyboard navigation and deletion preserve focus and selection.
Status cues and action/category/order parity, #3084 #2810 Distinct shapes identify all branch states without colour; invalid forms cannot Complete; dirty partial work can Save subject to permission and saving guards; category icons and visited validity match AF1; adding/deleting/reloading units preserves identity and creation order.
Controls and comments, 3A #3084 Every supported control presents prompt, helper, input and errors in that order; long prompts wrap; accessible names and descriptions stay associated; comment add/edit/cancel/save and quote insertion retain their semantics without hover/focus layout shifts. Unit regressions plus gallery browser screenshots.
Panels and navigation, 3B #3084; PDF-host owner integration Header buttons have unclipped focus surfaces; entity dialog Escape/backdrop/close preserve drafts and restore focus; My Studies design footnote removed while project permission and dirty navigation guards remain; Skip occupies a consistent header location across all stage types with unchanged behaviour.
Release baseline, 4 3A, 3B Annotation-only, screening-only, combined; narrow/wide; light/dark; pristine/dirty/invalid/completed; navigator on/off. Include nested branches, many entities, quote-to-comment and save failure. Required final-head CI and preview/staging journeys pass; deployed versions verified; user guide and rollback recorded.
Entity reordering, 5A Baseline; review-data ordering contract Drag and keyboard move actions persist order without changing entity identity, labels, answers or completion; reload, deletion, selection, failure and read-only paths covered.
Reviewer layouts, 5B Baseline; reviewed interaction and storage contract Per-reviewer versioned layouts for each capability combination; movable Source/Screening/Annotation panels; usable narrow-screen fallback, reset, schema/capability migration and failed persistence recovery.
Source modes, 5C 5B; PDF viewer lifecycle/host integration Embedded, floating and detached modes share authoritative study selection and preserve quotes; keyboard movement, viewport recovery, blocked-popup fallback, window close, study change and sign-out verified.
Backlog and cleanup, 6 Stable accepted default rollout Reconcile every #3067 entry with evidence or explicit follow-up; >8 branches usable; remove only unreferenced legacy components after rollback is no longer needed; preserve the original untracked notes and commit completion evidence.

The shortest critical path is #2810 → #3084 → 3A/3B → baseline acceptance. Shared shell files must be integrated after the active PDF-host changes; independent form work can proceed in parallel. Every implementation uses a sole-writer worktree under the repository’s .worktrees/ directory.

Flags and release gates

Existing annotationFormV2 and stageReviewRedesign gates remain in force. No production activation is implied by merging implementation. Wider/default enablement requires acceptance and a release decision, followed by a stability interval before legacy removal.

Entity order belongs to each reviewer’s review and must persist in that review’s database data. It must preserve entity names, identity and answer ownership. Preserve record-and-stay versus advance behaviour pending its separate decision. Reviewer-layout storage and mode/position persistence need a concrete reviewed contract. Credentials and production activation remain operator-controlled.

Infrastructure hardening, preview race repairs and unrelated colour/selector defects are only baseline blockers when current evidence proves an intrinsic correctness, regression or major security issue. Coordinate existing repair owners rather than duplicating them.

Recovery evidence

On 2026-09-05, #2810 remained open at 7d20971d1 with failed historical Angular checks;

3084 remained stacked on it at 932cba73c3. #3207 and #3174 are merged. Both branches merge

cleanly into the refreshed stack locally. Original pr/ worktrees have untracked status notes and are preserved. The main checkout also has unrelated staged/unstaged work; validation uses committed code in isolated worktrees.

Sources and implementation contracts

The acceptance criteria above govern delivery; prototypes illustrate interactions and do not replace these requirements.

Comment editing contract

Saved comment text appears in a compact chip with a Material icon-button edit action, retaining its expanded touch target and hover feedback. Add/Edit opens a local comment transaction held in the shared form store, so pagination and the narrowed Focus view do not lose it. Cancel drops that transaction; Save comment applies it to the review draft and closes the editor. Neither action writes to the server. Pending text participates in the existing unsaved-navigation guard.

Whole-session Save progress and Complete apply all pending comment edits before validation and submission. Revert, deletion and replacement by another session clear the relevant local editor state. Quotes inserted while editing remain in both the editor and its Cancel baseline: cancelling typed changes must not erase an independently inserted quote. Read-only sessions show comment text without edit controls.

Focus contract (revision 2 §6.7)

Focus is one feature with two entry scopes, and it is a state of the Annotation panel, never a dialog. The panel header stays with a "Focus" hint; everything else collapses to the 48px Focus bar, and the questions take the panel's full width. It works identically when the panel is grouped, maximised, floated, or opened in its own window, because it makes no assumption about its host.

  • Shown-set Focus is entered from the category line's Focus button, offered only while at least one entity is shown. It stacks every shown entity as a sticky-mini-header section. Previous/next scroll between those mini-headers — the current one is determined by scroll position and named beside the chevrons — and never swap entities. A mini-header's filter_center_focus narrows Focus to that entity; its × hides the entity and Focus continues with one fewer. When none remain, an information strip explains how to pick entities; Focus does not close itself. The bar's ⋮ is the "Shown in Focus" menu: a checkbox item per entity that keeps the menu open, Show all, and Add.
  • Single-entity Focus is the shortcut entered from a section mini-header, an entity card's ⋮, or the entity menu. It shows that entity alone, previous/next move between entities, and the ⋮ is the entity menu with "Shown in Focus…" last.
  • Study uses the same Focus state: the same shared draft, the same Save and Complete, the same Back and Escape. Study is a singleton implicit context, so it carries no entity-set controls — no previous/next, no position, no entity menu — and no invented second entity. It does reach the category boundary, because continuing to the next category is navigation rather than an entity-set control: Study's single section is always the end of its questions, so the pill and the end-of-questions footer offer "Continue to ".
  • The bar's contents degrade by its own measured width (a ResizeObserver on the bar, not viewport breakpoints), dropping the position, then the category name, then the Unsaved chip, then the action labels, and finally ellipsising the entity name. Back, the entity icon and name, previous/next and the ⋮ are never removed, and everything dropped stays reachable in the ⋮ header line and the breadcrumb tooltip.
  • At the last entity the right chevron becomes the next-category pill and an end-of-questions footer repeats the move. An empty next category lands on its empty state inside Focus. At the very end of the annotation the pill reads "Finish".
  • Leaving Focus never discards anything. The same question forms stay mounted as the same component instances across entering and leaving, because Focus only changes which units the one workspace projection returns and the class on their wrapper. There is no overlay container, so there is nothing to move and nothing to re-render.
  • Bounded rendering is unchanged: shown-set Focus mounts the shown set intersected with the current card page, so Focus pages through the stack under the same cap the workspace uses.
  • The top annotation action bar is hidden while Focus is on. The Focus bar carries Save and Complete itself, under the same anchors and the same enable rules, and both surfaces call the same commands — there is one authoritative draft and one save path.
  • Escape leaves Focus first in the Escape cascade and claims the event, so the host's Maximise and Rearrange handling does not fire on the same key press. Back and Escape both return keyboard focus to the control Focus was entered from.
  • The panel header stays in Focus and carries a "Focus" hint, but that header belongs to the stage-review Dockview shell, not to the form. AF2 therefore publishes its Focus state through a focusStateChanged output — {scope, category, label}, or null when Focus is off — and the shell renders the hint from it. This is deliberately a description and not a handle: a host can draw a hint and nothing else. No host can enter or leave Focus, because that would make the panel chrome a second owner of what is mounted.

This supersedes the per-entity fullscreen dialog, including the Study fullscreen dialog proposed in #3394.

Branch-answer overflow preferences

The 2026-09-08 reviewer mockup selects wrapping quiet branch tabs (15a) as the default at every nesting level. This first slice changes layout only: existing status shapes, active underline, branch identity, selection and draft state remain shared and unchanged. Long labels fit the available width and retain their full accessible names. Flag decision: this is inside the existing annotationFormV2 feature; no separate flag is needed for the default tab layout correction.

The account-persisted reviewer preference is now delivered. Revision-2 §5's three illustrated radio cards write overflowMode (wrap | scroll | menu, default wrap) to ReviewerWorkspaceSettings; the review page hands it to the annotation form through the ANNOTATION_ROW_OVERFLOW injection token, and both rows the design names honour it — the protocol branch tabs and the §6.3 entity cards. They are render choices over the same branch data and status logic, not project-admin settings and not the Dockview panel tabs.

Mode Branch tabs and entity cards
wrap Every control stays on screen, wrapping onto more rows. The default, and the behaviour of the first slice.
scroll One row, horizontally scrollable, with a chevron button at each end that disables when there is nothing left that way. overflow-y is pinned to hidden so Material's -1px ink-bar margin cannot spawn a vertical scroller.
menu As many controls as fit, then a +N more menu holding the rest. The row is measured with a ResizeObserver plus a MutationObserver, so the count follows both the row's width and the controls in it.

Switching modes preserves selection, drafts, focus, error reveal and accessibility. Collapsed controls keep their space in the layout and are hidden by visibility, which takes them out of the tab order and the accessibility tree; the +N more menu is what puts them back, with the branch tabs' selection semantics and the entity cards' shown/hidden checkbox semantics and reordering intact. Arrow-key navigation walks the tabs still on the row rather than stepping into a collapsed one that cannot take focus. Wrapping chips (15b) are an unselected alternative.

The mockup references an unspecified 10a selector beyond approximately15 branches. Neither that selector nor an exact cutover is supplied; the older >8 note in repository docs is not a settled threshold. Obtain that design before implementing the cutover. Do not conflate it with the existing ancestry selector.

Status: all three modes implemented for both rows, driven by the persisted preference. Browser coverage checks 375px nested wrapping, visible status indicators, stable tab IDs, keyboard selection and preserved nested drafts; preview acceptance remains pending. Flag decision: no new flag — every mode is inside the existing annotationFormV2 feature, and the preferences dialog that sets the choice is inside the existing default-off stageReviewRedesign flag. Hosts with no reviewer (annotation preview, reconcile) keep the wrap default. The unresolved 10a selector cutover below is still outstanding.

The branch-wrapping MVP keeps Add on the same row when short tabs leave room; longer sibling lists wrap without losing their status markers. The browser contract checks both a short row and actual multiple-row geometry at 375px.

Tab-strip hairline and label clipping (confirmed 2026-09-08)

Two review questions on the wrapping default were settled by measurement in a headless browser rather than by preference:

  • The hairline underlines the tablist, not the whole strip. It sits on .branch-tabs and stops at the tablist's right edge, so it does not run under Add when Add shares the row. The alternatives are worse: the strip wraps, so a border on .branch-tab-strip lands under the last row only -- the Add row once Add drops below the tabs, leaving the tabs unmarked. Forcing a single row with flex-wrap: nowrap restores a full-width hairline but was rejected: below roughly 300px the flex: 1 1 auto tablist collapses to 0px and every branch tab disappears, defeating the point of wrapping.
  • Tab labels clip without an extra min-width: 0. overflow: hidden on .branch-tab-text already zeroes the flex item's automatic minimum size (CSS Sizing: the content-based minimum is 0 when overflow is not visible), so the label shrinks inside the capped tab and ellipsises. Measured at 320px: a 452px label inside a 270px content box, clipped. .branch-add span works by the same rule, not because of min-width: 0 on its parent.

Regression coverage for the tab-label clipping case is tracked as a follow-up; the browser spec currently asserts clipping for the Add label only.

Review navigation waits for the current screening write to settle: Skip/Next and My Studies cannot leave while a decision is in flight, and only the matching intent, study, stage and reviewer outcome releases that guard. Study-keyed cached decisions are accepted only for the current stage and reviewer. Releasing a screened study's reservation uses the annotation-statistics transaction and its durable tracking-mode agreement, keeping fresh projections consistent with the source study.

Validation fixture cleanup

Consolidate the small duplicated valid-layout builders in API/controller and Mongo repository tests as a maintenance follow-up after workspace acceptance. The independently tested serialization boundaries remain covered; sharing those fixtures is not a runtime correctness requirement for the current delivery. The same follow-up should introduce a shared valid/invalid JSON corpus for the client and API validators; current boundary cases have matching focused tests.