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/discard and quote insertion keep their semantics (the comment is a normal field -- see the comment editing contract below) 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¶
- AF2 delivery overview and feature brief.
- Annotation question extensibility decisions and delivery map.
- Accelerated stage-review requirements.
- Remaining-work tracking issue #3067.
- Reviewer-specific entity ordering, #3224.
- Reviewer layout persistence, #3225 and Study Source modes, #3230.
The acceptance criteria above govern delivery; prototypes illustrate interactions and do not replace these requirements.
Comment editing contract¶
Owner-directed deviation from the v4 prototype (2026-10-05)
The v4 review prototype draws a Cancel / Save comment pair under the editor. The owner directed that the AF2 comment behave like a normal field instead, because "Save comment" implied a server save that does not happen: it only ever applied the text to the local review draft, and nothing reaches the server until the form's Save progress or Complete. The pair is removed; the contract below replaces it. Do not restore the buttons to match the prototype.
Saved comment text appears in a compact chip. When the session is editable the whole
chip (comment icon, text and a trailing pencil hint) is one native button named
"Edit comment for {question}: {text}" with aria-expanded="false", so clicking the
comment chip opens the editor; read-only sessions render the same chip as static
markup with nothing to press. To copy a comment's text, copy it from the editor.
Add comment or the chip opens a
local comment transaction held in the shared form store, so pagination and the
narrowed Focus view do not lose it. While the editor is open it is the only
comment control; there is no separate Hide button.
- Leaving the field keeps the text. Moving focus out of the editor -- Tab, a click anywhere else, or focus going to the quote toolbar's menu -- applies the text to the review draft (never the server) and collapses the editor back to the chip. Ctrl/Cmd+Enter does the same and returns focus to the chip. A press on the field's own padding, label or hint is inside the editor and does not end it.
- Esc discards. Escape drops what was typed since the editor opened,
collapses it and returns focus to the chip or Add comment. The editor owns the
keypress: it does not also close the dialog, panel or Focus mode around it, and an
Escape that cancels an input-method composition is left alone. A short hint,
"Esc to discard changes", sits under the field and is associated with it through
aria-describedby. - Clearing the text and leaving removes the comment, and Add comment is offered again.
- Losing the window is not leaving the field. A focus loss with no new focus
target while the document itself has lost focus (alt-tab, developer tools, the
detached Study Source window) leaves the editor open and uncommitted; focus
returns with the window. The check uses the injected
DOCUMENT, which follows a dock panel into its own browser window. - A pointer press elsewhere does not collapse the editor until it completes.
A mouse press moves focus on
mousedown, before itsclick; collapsing the editor then would shift everything below it and the click would land on whatever moved under the pointer. When focus leaves during a primary-button press (mouse, pen or touch), the commit waits for that press'spointerup/pointercancel(a touch tap's compatibilitymouseup) plus one timer tick, so the click dispatches against the unshifted layout first. The press is tracked on the document only while an editor is open, and the listeners are removed when it closes or the row is destroyed. Keyboard moves (Tab, programmatic focus) are not affected and still commit promptly, and a keystroke or context menu ends a tracked press so a lost release cannot delay them. The same guards apply when the deferred commit runs: destroyed row, recycled draft, finished transaction, or focus back in the editor all skip it. - A row being torn down is not leaving the field. Virtual-row recycling,
branch-tab switches and Focus mode destroy the focused textarea. That neither
commits nor discards: the transaction stays in the store's
commentEditsByDraftIdand the remounted row reopens it. The commit therefore runs a microtask after the focus loss and does nothing if the row was destroyed in the meantime, if focus has returned to the editor, or if the transaction has already been finished. - Pending text participates in the existing unsaved-navigation guard. The form counts an open edit whose text differs from the draft as a change while typing.
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 are merged into both the open editor and its baseline: discarding with Esc must not erase an independently inserted quote. Selecting study text in the source panel moves focus out of the editor, which commits and collapses it; Quote into comment... then appends to the committed comment and reopens the editor focused. A quote arriving while the editor is still open (for example from the detached source window) is merged with the typing and keeps the editor open. 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_focusnarrows 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
ResizeObserveron 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
focusStateChangedoutput —{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. |
The annotation form's category tab strip has its own account preference,
categoryOverflow (scroll | wrap, default scroll; a stored document without
the field reads as scroll, and a save that omits it keeps the stored value). It reaches the form through the
ANNOTATION_CATEGORY_OVERFLOW injection token, provided by the review page from
ReviewPreferencesStore.categoryOverflow; the annotation preview and reconcile
view keep the token default. scroll is Material's standard single-row tab nav
with its pagination chevrons; wrap adds .category-tabs--wrap, which wraps
.mat-mdc-tab-links and disables pagination. Flag decision: inside the existing
annotationFormV2 / stageReviewRedesign features; no separate flag.
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-tabsand 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-striplands under the last row only -- the Add row once Add drops below the tabs, leaving the tabs unmarked. Forcing a single row withflex-wrap: nowraprestores a full-width hairline but was rejected: below roughly 300px theflex: 1 1 autotablist collapses to 0px and every branch tab disappears, defeating the point of wrapping. - Tab labels clip without an extra
min-width: 0.overflow: hiddenon.branch-tab-textalready zeroes the flex item's automatic minimum size (CSS Sizing: the content-based minimum is 0 when overflow is notvisible), so the label shrinks inside the capped tab and ellipsises. Measured at 320px: a 452px label inside a 270px content box, clipped..branch-add spanworks by the same rule, not because ofmin-width: 0on 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.