Context
Surfaced by the cross-family review of PR #12892 (@neo-opus-grace) and confirmed + refined by @neo-fable (author/lead). #12892 renamed Neo.draggable.container.SortZone#onDragEnd → processDragEnd and made onDragEnd a synchronous re-entry latch routing exactly one drag:end delivery into processDragEnd. The grid header-toolbar SortZone was migrated to the new override-point; three sibling subclasses were not, leaving the base-class override-point contract followed by only 1 of 4 subclasses.
The Problem
Post-#12892 the base container/SortZone contract is: onDragEnd = the re-entry latch (a drag:end fans out across owner + child items → multiple deliveries; the latch admits one), processDragEnd = the drop logic subclasses extend. Three subclasses still override the legacy onDragEnd and call super.onDragEnd:
src/draggable/dashboard/SortZone.mjs:262 (+ self-invokes me.onDragEnd({}) at :312 / :349)
src/draggable/tab/header/toolbar/SortZone.mjs:40
src/draggable/table/header/toolbar/SortZone.mjs:58
Not a current regression (these were un-latched on dev before #12892 too, and super.onDragEnd → latch → processDragEnd still runs the base logic), but the consequences:
- They lose re-entry-latch protection for their OWN override logic — the double-execution the latch exists to prevent remains possible for tab/table/dashboard.
- The "subclasses extend
processDragEnd" contract is violated by 3 of 4 subclasses → maintenance hazard (the next SortZone subclass copies a sibling's onDragEnd override and perpetuates the drift).
The migration is not uniform: tab/table are clean mechanical renames; dashboard is a redesign (see Architectural Reality).
The Architectural Reality
- Base (
src/draggable/container/SortZone.mjs, post-#12892): onDragEnd(data) = latch (dragEndActive flag + try/finally → me.processDragEnd(data)); processDragEnd(data) = the drop pipeline.
- tab / table (
tab/header/toolbar/SortZone.mjs:40, table/header/toolbar/SortZone.mjs:58): both override onDragEnd and await super.onDragEnd(data) — a clean mechanical rename (onDragEnd → processDragEnd, super.onDragEnd → super.processDragEnd), mirroring the rename already applied to grid/header/toolbar/SortZone.mjs in #12892.
- dashboard (
dashboard/SortZone.mjs): overrides onDragEnd (:262, super.onDragEnd at :273) and self-invokes me.onDragEnd({}) at :312 / :349 as window-drag CLEANUP. A naive rename would route those cleanup self-calls through the base latch, changing their semantics. The redesign question (per @neo-fable, latch author): should the cleanup route through the public entry at all? Three candidate shapes (fable's framing, prior = (c)):
- (a) self-calls target
processDragEnd directly — deliberate latch bypass, cleanup always runs. Risk: re-opens double-execution for cleanup racing a real drag-end.
- (b) the latch carries an owner-token so the holder's own nested calls pass. Cost: machinery for a single caller.
- (c) extract the cleanup subset the :312/:349 self-calls actually need into its own method and call that. Rationale: the self-calls likely never wanted the full drop pipeline, just the restore subset. Whoever executes MUST verify which subset :312/:349 depend on first.
The Fix
Two separable units (one ticket, split ACs — execution may split across PRs):
- Mechanical (tab + table): rename
onDragEnd → processDragEnd, super.onDragEnd → super.processDragEnd. No behavior change; the base latch now wraps their drop logic.
- Dashboard redesign: resolve the self-call × latch interaction per shape (a)/(b)/(c), after verifying the :312/:349 cleanup dependency subset.
Context (#12894): the DeltaUpdates.moveNode Chromium-moveBefore heal now lives in the moveNode path dashboard's window-drag flows traverse — relevant when validating the dashboard cleanup behavior.
Decision Record impact
none — no ADR governs the SortZone override-point contract; this aligns the subclasses with the code-level contract #12892 established.
Acceptance Criteria
Out of Scope
- The grid
SortZone (already migrated in #12892).
- The
DeltaUpdates.moveNode Chromium-moveBefore heal hardening (that's #12894's own follow-up: scroll-preservation + gating).
- Any change to the base latch mechanism itself.
Related
- #12892 (introduced the override-point rename + latch; grid migrated) · #12894 (the #12883 reopen follow-up;
moveNode context for dashboard) · #12883 (the grid DnD ticket the parent work resolves).
- Review provenance: PR #12894 review
PRR_kwDODSospM8AAAABCsYyng + #12892 review PRR_kwDODSospM8AAAABCr-KvQ.
Release classification: post-release (Approve+Follow-Up follow-up — non-blocking).
Live latest-open sweep: checked latest 20 open issues at 2026-06-11T10:12Z; no equivalent found.
Origin Session ID: 0bc22bf2-9ded-4cdb-b351-182c3ab1d16f
Retrieval Hint: query_raw_memories("SortZone onDragEnd processDragEnd override-point migration dashboard latch self-call")
Authored by Claude Opus 4.8 (Claude Code), @neo-opus-grace.
Context
Surfaced by the cross-family review of PR #12892 (@neo-opus-grace) and confirmed + refined by @neo-fable (author/lead). #12892 renamed
Neo.draggable.container.SortZone#onDragEnd→processDragEndand madeonDragEnda synchronous re-entry latch routing exactly onedrag:enddelivery intoprocessDragEnd. The grid header-toolbar SortZone was migrated to the new override-point; three sibling subclasses were not, leaving the base-class override-point contract followed by only 1 of 4 subclasses.The Problem
Post-#12892 the base
container/SortZonecontract is:onDragEnd= the re-entry latch (adrag:endfans out across owner + child items → multiple deliveries; the latch admits one),processDragEnd= the drop logic subclasses extend. Three subclasses still override the legacyonDragEndand callsuper.onDragEnd:src/draggable/dashboard/SortZone.mjs:262(+ self-invokesme.onDragEnd({})at :312 / :349)src/draggable/tab/header/toolbar/SortZone.mjs:40src/draggable/table/header/toolbar/SortZone.mjs:58Not a current regression (these were un-latched on
devbefore #12892 too, andsuper.onDragEnd→ latch →processDragEndstill runs the base logic), but the consequences:processDragEnd" contract is violated by 3 of 4 subclasses → maintenance hazard (the next SortZone subclass copies a sibling'sonDragEndoverride and perpetuates the drift).The migration is not uniform: tab/table are clean mechanical renames; dashboard is a redesign (see Architectural Reality).
The Architectural Reality
src/draggable/container/SortZone.mjs, post-#12892):onDragEnd(data)= latch (dragEndActiveflag + try/finally →me.processDragEnd(data));processDragEnd(data)= the drop pipeline.tab/header/toolbar/SortZone.mjs:40,table/header/toolbar/SortZone.mjs:58): both overrideonDragEndandawait super.onDragEnd(data)— a clean mechanical rename (onDragEnd→processDragEnd,super.onDragEnd→super.processDragEnd), mirroring the rename already applied togrid/header/toolbar/SortZone.mjsin #12892.dashboard/SortZone.mjs): overridesonDragEnd(:262,super.onDragEndat :273) and self-invokesme.onDragEnd({})at :312 / :349 as window-drag CLEANUP. A naive rename would route those cleanup self-calls through the base latch, changing their semantics. The redesign question (per @neo-fable, latch author): should the cleanup route through the public entry at all? Three candidate shapes (fable's framing, prior = (c)):processDragEnddirectly — deliberate latch bypass, cleanup always runs. Risk: re-opens double-execution for cleanup racing a real drag-end.The Fix
Two separable units (one ticket, split ACs — execution may split across PRs):
onDragEnd→processDragEnd,super.onDragEnd→super.processDragEnd. No behavior change; the base latch now wraps their drop logic.Context (#12894): the
DeltaUpdates.moveNodeChromium-moveBeforeheal now lives in themoveNodepath dashboard's window-drag flows traverse — relevant when validating the dashboard cleanup behavior.Decision Record impact
none— no ADR governs the SortZone override-point contract; this aligns the subclasses with the code-level contract #12892 established.Acceptance Criteria
tab/header/toolbar/SortZoneoverridesprocessDragEnd(notonDragEnd) and callssuper.processDragEnd; behavior unchanged.table/header/toolbar/SortZoneoverridesprocessDragEnd(notonDragEnd) and callssuper.processDragEnd; behavior unchanged.SortZone: the :312 / :349 cleanup self-call dependency subset is identified (documented in the PR) before a shape is chosen.SortZone: the latch × self-call interaction is resolved (shape a/b/c) such that (i)drag:enddouble-delivery is latched and (ii) the window-drag cleanup self-calls run their intended subset without racing a real drag-end.onDragEndoverride remains outside the base latch across the 4container/SortZonesubclasses (grid already done).test/playwright/unit/draggable/container/SortZone.spec.mjs+ the tab/table/dashboard drag paths).Out of Scope
SortZone(already migrated in #12892).DeltaUpdates.moveNodeChromium-moveBeforeheal hardening (that's #12894's own follow-up: scroll-preservation + gating).Related
moveNodecontext for dashboard) · #12883 (the grid DnD ticket the parent work resolves).PRR_kwDODSospM8AAAABCsYyng+ #12892 reviewPRR_kwDODSospM8AAAABCr-KvQ.Release classification: post-release (Approve+Follow-Up follow-up — non-blocking).
Live latest-open sweep: checked latest 20 open issues at 2026-06-11T10:12Z; no equivalent found.
Origin Session ID: 0bc22bf2-9ded-4cdb-b351-182c3ab1d16f
Retrieval Hint:
query_raw_memories("SortZone onDragEnd processDragEnd override-point migration dashboard latch self-call")Authored by Claude Opus 4.8 (Claude Code), @neo-opus-grace.