Skip to content

fix(studio): resolve 8 confirmed adversarial-review findings across the flat-inspector stack#2416

Open
vanceingalls wants to merge 2 commits into
07-13-style_studio_distinguish_open-section_body_from_header_backgroundfrom
07-14-fix-studio-adversarial-review-followups
Open

fix(studio): resolve 8 confirmed adversarial-review findings across the flat-inspector stack#2416
vanceingalls wants to merge 2 commits into
07-13-style_studio_distinguish_open-section_body_from_header_backgroundfrom
07-14-fix-studio-adversarial-review-followups

Conversation

@vanceingalls

Copy link
Copy Markdown
Collaborator

What

Brief description of the change.

Why

Why is this change needed?

How

How was this implemented? Any notable design decisions?

Test plan

How was this tested?

  • Unit tests added/updated
  • Manual testing performed
  • Documentation updated (if applicable)

…he flat-inspector stack

Fixes issues raised in the Deepwork re-review of #2120-#2190 that weren't
covered by #2225's earlier fix pass:

- useColorGradingController: reset grading/compare/mediaMetadata state (and
  cancel pending persist/status timers) when selection changes to a
  different element — this hook is called unconditionally on every render
  (unlike legacy ColorGradingSection, remounted via a selectionIdentityKey
  React key), so switching selection reused the previous element's state.
- useColorGradingController: stop permanently caching a non-OK
  /media/metadata response as null — a transient server error poisoned the
  HDR banner for that asset for the whole page lifetime.
- FlatSelectRow: preserve a valid authored value outside the preset list
  (e.g. mix-blend-mode: difference, an arbitrary object-position) instead of
  silently misrepresenting it as the first preset — touching the control
  would overwrite real persisted state.
- FlatSlider: the throttled trailing commit now reads onCommit through a
  ref updated every render instead of closing over it at schedule time — a
  caller whose onCommit spreads other current state (Grade's per-detail
  commits) could otherwise have a delayed commit revert whatever the user
  changed on a different control in the same 40ms window.
- FlatSlider: flush a still-queued trailing commit on unmount instead of
  dropping it, and disable the reset button when the slider itself is
  disabled.
- FlatSlider: add touch-action: none to the track so touch drags don't
  compete with page scroll.
- FlatColorGradingAccessory: clean up the compare-hold's window listeners
  on unmount, not only on release — switching selection mid-hold used to
  leak them.
- Align (flat Text): re-clicking the option already visually active for a
  logical start/end value no longer rewrites it to the physical left/right,
  preserving RTL semantics.
- FlatSegmentedRow: give every option an accessible name and aria-pressed
  state — two visually-identical glyph buttons (upright/italic "A") had no
  way to be told apart by assistive tech.
- PropertyPanelFlat: the panel body falls back to its own scroll when the
  collapsed group headers alone exceed the available height, so groups
  can't become permanently unreachable in a short pane.

New regression tests for all of the above; full studio suite at the known
pre-existing baseline (55 failures unrelated to this stack).

Copy link
Copy Markdown
Collaborator Author

Warning

This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
Learn more

This stack of pull requests is managed by Graphite. Learn more about stacking.

@miguel-heygen miguel-heygen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed at 267cdfce1661b43467d1d0606b43f3629f120cc2, treating this tip fix PR as the authoritative cumulative resolution for the flat-inspector stack.

Blocking findings

  1. [BLOCKER] Selection identity still omits the source file. propertyPanelHelpers.ts:37-45 builds selectionIdentityKey from only id, hfId, selector, and selectorIndex, while the canonical edit-target key includes sourceFile (domEditingLayers.ts:564-573). useColorGradingController.ts:200-229 now depends on the incomplete key to decide whether to flush/reset all per-element state. Two elements in different composition source files with the same local identity therefore reuse grading, compare, metadata, and pending state. Include sourceFile (preferably reuse the canonical key) and add a cross-source collision regression.

  2. [BLOCKER] Changing selection silently discards a pending Grade edit. In useColorGradingController.ts:211-218, an identity change clears the 350ms persistence timer and sets pendingPersistValueRef.current = undefined. The accompanying test explicitly expects no write after this transition. That prevents stale work from hitting the new element, but loses the user's last edit to the old element. Flush against a captured old target before switching (or persist each edit through an identity-bound queue) and test that selecting away inside the debounce window saves the old element without touching the new one.

  3. [BLOCKER] Persistence rejection still cannot reconcile optimistic slider/controller state. FlatSlider owns draft and only resynchronizes when the scalar value dependency changes (propertyPanelFlatPrimitives.tsx:280-308). If a commit rejects while the controlled value remains the original value, that effect never reruns, so the knob/ARIA value stays at the unsaved attempt. Grade has the same wider form: commitColorGrading updates controller/runtime state before its fire-and-forget persist, while the persistence seam rolls back only the document. Add an acknowledged failure/revision path that restores the authoritative value and pin rejected-write behavior.

  4. [BLOCKER] Disabled Style selects still mutate through Reset. FlatSelectRow disables only the <select> (propertyPanelFlatPrimitives.tsx:503-535); its explicit-custom reset button remains enabled and calls onReset. Style supplies both disabled={!canEditStyles} and mutating reset callbacks for Shadow, Blend, Overflow, and Mask, so a locked/uneditable target can still be changed. Propagate disabled to the reset button and guard the callback; add a regression.

Important follow-ups

  • FlatSelectRow comboboxes still have no accessible name: the visible label is a sibling <span>, while the wrapping <label> contains only the select and chevron. Associate them with htmlFor/id or aria-label.
  • FlatSlider handles pointercancel but not unexpected lostpointercapture; capture loss can leave draggingRef true and suppress controlled-value synchronization indefinitely.
  • The PR body is still the untouched template despite a +550/−142 behavioral fix. Replace it with the actual eight claimed fixes, stack role, and test plan before merge.

Verified strengths and scope

  • Audited all 11 files in this PR and the cumulative caller/contract sites from #2225 through #2416; historical stack code outside those targeted seams was trusted only where patch-equivalence had already been established.
  • The tip correctly fixes custom select value representation, slider keyboard/touch behavior, stale Grade callback capture, unmount flushing, compare-listener cleanup, logical alignment preservation, transient metadata-cache poisoning, and constrained-height scrolling.
  • Focused local verification passed: 51/51 tests across propertyPanelFlatPrimitives, propertyPanelFlatTextSection, and useColorGradingController. GitHub preview/player/regression checks are green/skipped, but Graphite mergeability is still pending because the downstack remains open.

Verdict: REQUEST CHANGES
Reasoning: A tip fix PR is an acceptable resolution strategy, but this tip still loses pending Grade edits on selection change, uses a cross-source-colliding identity key, cannot roll back optimistic UI after rejected persistence, and permits mutations through disabled select resets. Resolve those cumulative-state blockers and complete the PR description before approval.

— Deepwork

…review

Fixes the Deepwork tip re-review's four remaining blockers plus its
additive findings:

- selectionIdentityKey: add sourceFile as a 5th identity component. The
  same local id/selector can legitimately recur across different
  composition files (host vs. an inlined sub-composition, or two unrelated
  sub-comps) — without sourceFile, those collided onto the same identity
  key and reused stale controller state across a selection change that
  should have reset it.
- useColorGradingController: flush (not discard) a pending Grade edit when
  selection changes before the 350ms debounce fires. The prior fix
  correctly stopped it from landing on the WRONG (new) target, but
  cancelling outright silently dropped the user's in-flight edit instead of
  writing it to the element it was authored for — using the
  onSetAttributeLive closure captured for the outgoing render, which
  (via commitDataAttribute's own useCallback deps) is still bound to the
  outgoing selection.
- useColorGradingController: revert to the last confirmed-good grading when
  a persist rejects, instead of leaving the optimistic (never-actually-
  saved) value showing indefinitely. Tracks a separate
  confirmedGradingRef, updated only on a successful persist.
- FlatSelectRow: disable the reset button when the row itself is disabled
  (it previously ignored disabled entirely, same class of bug as the
  FlatSlider reset button fixed earlier) and give the underlying <select>
  an aria-label from the row's label text.
- FlatSlider: handle lostpointercapture the same as pointercancel — capture
  can be lost without either firing first (another element steals it, or
  the browser reclaims it for a scroll/touch gesture), which previously
  left the dragging flag stuck and the knob permanently unable to sync to
  external value changes.

New regression tests for all of the above; full studio suite still at the
known pre-existing baseline (55 failures unrelated to this stack).
@vanceingalls

Copy link
Copy Markdown
Collaborator Author

All four cumulative blockers fixed at 6f40e03a1:

selectionIdentityKey omits sourceFile — added as a 5th identity component. Same local id/selector recurring across different composition files (host vs. an inlined sub-comp, or two unrelated sub-comps) no longer collides onto the same key. New test: also resets when the same local id/selector recurs in a different source file.

Selecting away inside the 350ms Grade window discards the pending edit — you're right that the previous fix over-corrected. It now flushes instead: the pending value writes through the onSetAttributeLive closure captured for the outgoing render (which — via commitDataAttribute's own useCallback deps on domEditSelection — is still bound to the outgoing element, confirmed by reading that hook directly), not the newly-selected one. New test: flushes — rather than discards — a pending persist for the previous element when selection changes before it fires.

Rejected persistence can't restore optimistic Grade state — added a confirmedGradingRef, updated only on a successful persist. On rejection, grading reverts to that last-confirmed value and runtimeStatus reflects the failure, instead of the optimistic (never-saved) value sticking around indefinitely. New test: reverts to the last confirmed-good grading when a persist rejects. (This was the FlatSlider-level version of the same finding, previously deferred on #2190 as a broader contract change — the Grade-controller-level fix here is scoped and doesn't require touching FlatSlider's onCommit signature, since the revert lives entirely in useColorGradingController.)

Disabled Style selects still mutate through ResetFlatSelectRow's reset button now takes disabled={disabled}, same class of gap the FlatSlider reset button had.

Also addressed the two flagged (non-blocking) items:

  • Unnamed select controlsFlatSelectRow's <select> now gets aria-label={label}.
  • Missing lostpointercaptureFlatSlider now handles onLostPointerCapture the same as onPointerCancel; capture can be lost without either firing first (another element steals it, or the browser reclaims it for a scroll/touch gesture), which previously left the dragging flag stuck.

New regression tests for all of the above (12 new/updated tests across useColorGradingController.test.ts and propertyPanelFlatPrimitives.test.tsx); full studio suite still at the known pre-existing 55-failure baseline, zero regressions.

Not touched: the PR template and the Windows install-lock concurrency tests flagged on #2120 — those are pre-existing/unrelated to this stack's changes, happy to take a separate pass if you want them addressed here too.

@miguel-heygen miguel-heygen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the exact head 6f40e03a15bacf4c9e83a45237374bec80c57b3d, including the full 12-file cumulative diff (+742/-153) and the five-file R1→R2 delta (+196/-15). The sourceFile state key, disabled select reset, lostpointercapture handler, and single-transition pending-edit test are useful corrections. The remaining failures are at the boundaries the new tests mock away.

Blocking findings

  1. [P1] The Grade rollback cannot observe a real Studio persistence failure. useColorGradingController.ts:326-344 treats rejection from onSetAttributeLive as its failure signal. Production supplies handleDomAttributeLiveCommit (useDomEditAttributeCommits.ts:155-163), which awaits commitDataAttribute; runDomEditCommit catches persistence errors and returns normally (domEditCommitRunner.ts:31-45). On a disk/API failure the controller therefore executes the success branch at useColorGradingController.ts:330-333, marks the rejected optimistic grade confirmed, and never shows/reverts through the new failure branch. useColorGradingController.test.ts:132-153 injects a directly rejecting mock, so it proves a contract the real adapter does not provide. Please propagate an explicit success/failure result (or rejection) across the production seam and test through that adapter.

  2. [P1] Cross-source identity is fixed for React state but not for the Grade runtime target. propertyPanelHelpers.ts:40-49 now correctly distinguishes identical local targets by sourceFile, but the target built at useColorGradingController.ts:254-262 still carries only hfId/id/selector/selectorIndex; HfColorGradingTarget has no source discriminator (packages/core/src/colorGrading.ts:94-99). Runtime resolution takes the first global matching data-hf-id/ID (packages/core/src/runtime/colorGrading.ts:1014-1043). With sub-comp-a.html#bg and sub-comp-b.html#bg, persistence can target B through the source-bound callback while live preview/status/compare operate on A. The new test only checks hook-state reset and has no iframe/runtime assertion. The runtime contract needs the same source identity (or another unambiguous rendered-instance key) plus a negative collision test.

  3. [P1] Selection-change flushing performs an external persistence side effect during render. The identity branch at useColorGradingController.ts:223-237 calls previousOnSetAttributeLive(...) synchronously while React is rendering. StrictMode replay, an interrupted render, or an abandoned transition can duplicate that write or persist an edit for a selection change that never commits. The new test (useColorGradingController.test.ts:204-234) is not under StrictMode and uses one stable mock callback, so it cannot exercise replay or the outgoing-target closure. Move the flush to a commit-phase lifecycle/event boundary and make it idempotent.

  4. [P1] Async Grade completion state is neither selection-scoped nor latest-write-wins. Every completion mutates the shared confirmedGradingRef (and rejection mutates current UI) at useColorGradingController.ts:326-344. A save for element A can settle after selection moved to B and overwrite B's confirmed baseline; two saves for one element can settle out of order and regress the baseline to the older value. A later failure then reverts the current control to the wrong element/value. Capture selection identity plus an attempt/version token and ignore stale completions; add deferred-promise tests for selection changes and out-of-order resolve/reject.

Important follow-ups in this delta

  • The accessible-name test exercises FlatSelectRow(label="Shadow"), but the real Grade preset passes label="" (propertyPanelFlatColorGradingSection.tsx:311-322), and the LUT select at :359-374 is also unnamed. aria-label={label || undefined} at propertyPanelFlatPrimitives.tsx:515-520 does not fix those production controls.
  • The new capture-loss test orders loss before the prop update. The hostile order is drag → controlled value changes while synchronization is suppressed → capture loss. onLostPointerCapture only clears draggingRef, so with no later prop change the draft stays stale and the next keyboard action can overwrite the newer value.

Verification

  • Focused exact-head tests: 3 files, 55/55 passed (useColorGradingController, propertyPanelFlatPrimitives, propertyPanelFlatTextSection).
  • GitHub checks are green/skipped for Preview parity, preview regression, regression, and player perf; Graphite mergeability remains pending because the downstack is open. The PR is mergeable but UNSTABLE and still has the prior changes-requested gate.
  • Audited: all five files in the R1→R2 semantic delta and their production caller/runtime contracts. Trusting from the prior full pass: unchanged portions of the other seven cumulative-diff files.

Verdict: REQUEST CHANGES

Reasoning: Several local fixes are correct, but persistence failure handling is disconnected from the real callback contract, source identity still diverges between persistence and runtime preview, and render/async races can write or restore the wrong Grade state. These are user-visible correctness failures on the exact boundaries this fix PR claims to close.

— Deepwork

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants