Context
Found while falsifying the mechanism my own #18553 prescribed. That ticket asserts the topologyFingerprint answers "have you left the default layout". I ran the falsifier instead of taking the assertion, and the fingerprint turned out to be shape-only — which led to the differ beside it, which turned out to be blind on a first-class gesture.
This is #18255 one axis over. That ticket found the diff had no active-item category, so a restore "brings the tabs back, but not the tab you were on". Its body lists what the planner emits — addTab, moveItem, resizeSplit, setItemAutoHidden — and resizeEdgeZone is absent from that list. Nobody, including me reading it today, noticed the second omission.
Live latest-open sweep: latest 20 open issues at 2026-09-10T11:50:28Z; no equivalent found. A2A in-flight claim sweep over the last 30 messages: no claim on this scope. Memory Core rationale sweep on the observed symptom returned only the #17845 / #17849 splitter drag-bounds work (live clamp semantics), a different layer with no decision touching this. Own-assignment sweep: 14 open, none on the dock diff/planner surface.
The Problem
Measured at dev@2fdc57af69, against the real apps/workstation/tour/denseWorkstation.mjs initialDocument, applying real Operations and reading both instruments:
| user operation |
change class |
shape fingerprint |
diffDockDocuments |
| drag the main split 0.6/0.4 → 0.25/0.75 |
geometry |
unchanged |
resizes |
| drag the left rail 0.11 → 0.4 |
geometry |
unchanged |
NOTHING |
| switch the active tab |
topology |
unchanged |
activeItemChanges |
| reorder a tab in place |
topology |
unchanged |
tabReorders |
| lock a pane |
itemFlags |
unchanged |
NOTHING |
| relocate a pane across nodes |
topology |
changed |
moves |
| close a pane |
topology |
changed |
removes |
The rail drag is the only layout gesture no instrument sees. And because RestorePlanner.planRestore is a pure fold over that diff, the consequence is not a missing report — it is a silent no-op restore. Same document pair, only the left rail differing:
captured left extent : 0.11
live left extent : 0.4
documents differ : true
deferred : false (null)
errors : []
plan : []
deferred: false, errors: [], plan: [] — the planner reports success with nothing to do. A consumer folding that plan concludes the layout is already restored. So a user who widens a rail, saves nothing, and restores a saved perspective gets every pane back at the captured position and the rail left where they dragged it, with no error anywhere.
initialDocument marks three of four edges resizable: true, so this is the shipped flagship's own configuration, not a corner case.
The Architectural Reality
The node walk in diffDockDocuments examines exactly two node types and returns before reaching the third:
if (beforeNode?.type !== 'split' || afterNode?.type !== 'split') return;
edge-zone nodes are therefore never inspected, and extent lives on the descriptor, not the node it points at — WorkspaceDocument.dockZoneDescriptorKeys = new Set(['nodeId', 'extent', 'resizable']), validated finite in the open interval (0, 1). So the value is durable committed document state that the differ structurally cannot reach.
The asymmetry is not a considered contract. Operations.operationChangeClass classes the two resize verbs identically:
resizeSplit : 'geometry',
resizeEdgeZone: 'geometry',
One of that pair is reported and one is not, and no comment anywhere argues that an edge extent is runtime-only — the capture format keeps it, which is the opposite argument. That is the same reasoning #18255 used to establish the active item was unaddressed rather than declined.
Downstream, RestorePlanner maps each category to its operation at :123-129 (setActiveItem, resizeSplit, setItemAutoHidden). There is no arm to add resizeEdgeZone to, because the category it would read does not exist.
Two consumers inherit this, not one:
persistence/RestorePlanner.mjs:71 — every perspective restore.
src/ai/client/DockService.mjs:243 — the Neural Link diff_dock_topology MCP tool, whose JSDoc already scopes its tolerance to "split size fractions". An agent asking what changed after a rail drag is told nothing changed.
A stale docblock is part of this. TopologyDiff's class comment enumerates seven categories; the code returns eight — activeItemChanges, added by #18255, was never added to the prose. Hand-copying that list from the docblock under-reported the setActiveItem row in my first measurement pass, which is how a category omission stays invisible to the next reader.
The Fix
TopologyDiff.diffDockDocuments gains an edge-zone extent category, in the same {nodeId, ...} shape as the existing ones — the walk stops short-circuiting on edge-zone and compares each zone descriptor's extent under the same epsilon discipline resizes already uses.
RestorePlanner.planRestore emits resizeEdgeZone for each, ordered with the other geometry steps (after adds/moves/reorders — geometry applies to a settled tree).
- The class docblock's category list is brought back into agreement with what the code returns,
activeItemChanges included.
Nothing new in the model: Operations.resizeEdgeZone is already implemented, registered and classed. This is a diff category and one plan step, exactly the shape #18255 landed.
Contract Ledger Matrix
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback |
Docs |
Evidence |
TopologyDiff.diffDockDocuments result |
model/TopologyDiff.mjs:103 empty() |
one added category array, additive; existing categories byte-identical |
absent/malformed zones fails closed through the existing shape gate, as today |
class docblock category list, corrected to match the code |
probe table above; existing DockTopologyDiff.spec.mjs must stay green unchanged |
sizeEpsilon option |
diffDockDocuments({sizeEpsilon}), SIZE_EPSILON = 0.001 |
reused for extents — both are fractions in (0, 1), so one tolerance stays honest; a second knob would be a contract nobody asked for |
omitted → SIZE_EPSILON, as today |
JSDoc drops "on split size fractions" |
dockZoneDescriptorKeys validates extent in the same open interval as split sizes |
RestorePlanner.planRestore plan |
persistence/RestorePlanner.mjs:114-129 |
may now contain resizeEdgeZone steps |
a document with no edge zones plans exactly as today |
planner ordering comment |
Operations.operationChangeClass:94 already classes the verb |
diff_dock_topology MCP tool |
src/ai/client/DockService.mjs:17,243 |
reports the new category; no signature change |
unchanged |
tool JSDoc |
pass-through call site, no local shaping |
Acceptance Criteria
Out of Scope
setItemLocked invisibility — the diff has no lock category either, but locking is an item policy rather than a layout geometry, and whether a restore should re-apply it is a question nobody has asked. Measured here and deliberately left: a separate finding, not a silent fold-in.
The computeShapeFingerprint blindness itself. Shape-only is that function's documented and correct contract — it is the guardrail for windowFingerprint, and widening it would break the equality it exists to provide. #18553 stops prescribing it; the fingerprint does not change.
Live drag bounds and clamp semantics (#17845 / #17849 lineage). This is the model/plan tier, not the interactive resize tier.
Avoided Traps
⚠️ Do not solve this consumer-side. Three call sites read this differ; a consumer-side extent comparison in #18553's indicator would leave RestorePlanner and the MCP tool exactly as broken, and would hand-write in the app what the model already owns.
⚠️ Do not fold extents into the existing resizes array. Its entries carry fromSizes/toSizes arrays keyed by split node; an edge zone changes one scalar on one keyed zone of a node. Reusing the array forces consumers to type-sniff entries, and RestorePlanner maps category → operation one-to-one — a mixed array breaks that mapping.
⚠️ Do not widen the shape fingerprint to notice extents so the gate catches it. planRestore uses shape equality to decide whether the topologies are comparable at all; making a rail drag a shape mismatch would turn an ordinary restore into a topology-fingerprint-mismatch deferral.
⚠️ Do not read the class docblock as the category list. It is stale by one, which is precisely how this stayed unseen — derive categories from the returned object.
Related
- #18255 — the same defect on the active-item axis; its fix pattern applies verbatim. Closed COMPLETED.
- #18177 — extent loss on the tear-out/come-home path (
captureNodeHome / restoreNodeHome). Different mechanism, same user-visible cost; establishes that losing a width the user set is treated as a defect here. Closed COMPLETED.
- #18206 — resize declared
geometry but never emitted; the change-class map this ticket cites. Closed COMPLETED.
- #18553 — blocked on this for its AC-2 mechanism; its body is being corrected to stop prescribing the fingerprint.
- #18547 — the rail overlay extent resolver, projection tier. Adjacent surface, different layer.
Decision Record impact
aligned-with ADR 0029 — same alignment #18255 claimed for the identical shape: an existing model verb reaches an existing plan, no new concept.
Retrieval Hint: edge zone extent restore topology diff resizeEdgeZone planRestore empty plan rail width
Origin Session ID: e2441cf8-c250-4d78-b8c6-d6fa1624838c
Context
Found while falsifying the mechanism my own #18553 prescribed. That ticket asserts the
topologyFingerprintanswers "have you left the default layout". I ran the falsifier instead of taking the assertion, and the fingerprint turned out to be shape-only — which led to the differ beside it, which turned out to be blind on a first-class gesture.This is #18255 one axis over. That ticket found the diff had no active-item category, so a restore "brings the tabs back, but not the tab you were on". Its body lists what the planner emits —
addTab,moveItem,resizeSplit,setItemAutoHidden— andresizeEdgeZoneis absent from that list. Nobody, including me reading it today, noticed the second omission.Live latest-open sweep: latest 20 open issues at
2026-09-10T11:50:28Z; no equivalent found. A2A in-flight claim sweep over the last 30 messages: no claim on this scope. Memory Core rationale sweep on the observed symptom returned only the #17845 / #17849 splitter drag-bounds work (live clamp semantics), a different layer with no decision touching this. Own-assignment sweep: 14 open, none on the dock diff/planner surface.The Problem
Measured at
dev@2fdc57af69, against the realapps/workstation/tour/denseWorkstation.mjsinitialDocument, applying realOperationsand reading both instruments:diffDockDocumentsgeometryresizesgeometrytopologyactiveItemChangestopologytabReordersitemFlagstopologymovestopologyremovesThe rail drag is the only layout gesture no instrument sees. And because
RestorePlanner.planRestoreis a pure fold over that diff, the consequence is not a missing report — it is a silent no-op restore. Same document pair, only the left rail differing:deferred: false,errors: [],plan: []— the planner reports success with nothing to do. A consumer folding that plan concludes the layout is already restored. So a user who widens a rail, saves nothing, and restores a saved perspective gets every pane back at the captured position and the rail left where they dragged it, with no error anywhere.initialDocumentmarks three of four edgesresizable: true, so this is the shipped flagship's own configuration, not a corner case.The Architectural Reality
The node walk in
diffDockDocumentsexamines exactly two node types and returns before reaching the third:// src/dashboard/dock/model/TopologyDiff.mjs:182 if (beforeNode?.type !== 'split' || afterNode?.type !== 'split') return;edge-zonenodes are therefore never inspected, andextentlives on the descriptor, not the node it points at —WorkspaceDocument.dockZoneDescriptorKeys = new Set(['nodeId', 'extent', 'resizable']), validated finite in the open interval(0, 1). So the value is durable committed document state that the differ structurally cannot reach.The asymmetry is not a considered contract.
Operations.operationChangeClassclasses the two resize verbs identically:// src/dashboard/dock/model/Operations.mjs:93-94 resizeSplit : 'geometry', resizeEdgeZone: 'geometry',One of that pair is reported and one is not, and no comment anywhere argues that an edge extent is runtime-only — the capture format keeps it, which is the opposite argument. That is the same reasoning #18255 used to establish the active item was unaddressed rather than declined.
Downstream,
RestorePlannermaps each category to its operation at:123-129(setActiveItem,resizeSplit,setItemAutoHidden). There is no arm to addresizeEdgeZoneto, because the category it would read does not exist.Two consumers inherit this, not one:
persistence/RestorePlanner.mjs:71— every perspective restore.src/ai/client/DockService.mjs:243— the Neural Linkdiff_dock_topologyMCP tool, whose JSDoc already scopes its tolerance to "split size fractions". An agent asking what changed after a rail drag is told nothing changed.A stale docblock is part of this.
TopologyDiff's class comment enumerates seven categories; the code returns eight —activeItemChanges, added by #18255, was never added to the prose. Hand-copying that list from the docblock under-reported thesetActiveItemrow in my first measurement pass, which is how a category omission stays invisible to the next reader.The Fix
TopologyDiff.diffDockDocumentsgains an edge-zone extent category, in the same{nodeId, ...}shape as the existing ones — the walk stops short-circuiting onedge-zoneand compares each zone descriptor'sextentunder the same epsilon disciplineresizesalready uses.RestorePlanner.planRestoreemitsresizeEdgeZonefor each, ordered with the other geometry steps (after adds/moves/reorders — geometry applies to a settled tree).activeItemChangesincluded.Nothing new in the model:
Operations.resizeEdgeZoneis already implemented, registered and classed. This is a diff category and one plan step, exactly the shape #18255 landed.Contract Ledger Matrix
TopologyDiff.diffDockDocumentsresultmodel/TopologyDiff.mjs:103empty()zonesfails closed through the existing shape gate, as todayDockTopologyDiff.spec.mjsmust stay green unchangedsizeEpsilonoptiondiffDockDocuments({sizeEpsilon}),SIZE_EPSILON = 0.001(0, 1), so one tolerance stays honest; a second knob would be a contract nobody asked forSIZE_EPSILON, as todaydockZoneDescriptorKeysvalidatesextentin the same open interval as split sizesRestorePlanner.planRestoreplanpersistence/RestorePlanner.mjs:114-129resizeEdgeZonestepsOperations.operationChangeClass:94already classes the verbdiff_dock_topologyMCP toolsrc/ai/client/DockService.mjs:17,243Acceptance Criteria
extentproduces a non-empty diff naming that zone. Red-first: the arm pins today's empty result and flips.planRestore(dragged, captured)emits aresizeEdgeZonestep; today it returnsdeferred: false, errors: [], plan: []and that triple is what makes the defect silent. An arm asserting only the diff category leaves the user-visible half unwitnessed.resizes. Restore plans stay minimal; a no-op step is a spurious commit.extenton one or both sides emits no step and no error; absence is not a change to zero.resizableis not extent. A descriptor whoseresizableflag differs but whoseextentmatches emits no resize step — the fix must not smuggle a policy flag into a geometry category.DockTopologyDiff.spec.mjsandDockRestorePlanner.spec.mjsarm stays green unmodified. The category is additive; a spec that had to be edited to pass means the shape changed.TopologyDiffclass docblock enumerates exactly the categories the code returns,activeItemChangesincluded. The drift is what hid this omission from its own readers.Out of Scope
setItemLockedinvisibility — the diff has no lock category either, but locking is an item policy rather than a layout geometry, and whether a restore should re-apply it is a question nobody has asked. Measured here and deliberately left: a separate finding, not a silent fold-in.The
computeShapeFingerprintblindness itself. Shape-only is that function's documented and correct contract — it is the guardrail forwindowFingerprint, and widening it would break the equality it exists to provide. #18553 stops prescribing it; the fingerprint does not change.Live drag bounds and clamp semantics (#17845 / #17849 lineage). This is the model/plan tier, not the interactive resize tier.
Avoided Traps
⚠️ Do not solve this consumer-side. Three call sites read this differ; a consumer-side extent comparison in #18553's indicator would leave
RestorePlannerand the MCP tool exactly as broken, and would hand-write in the app what the model already owns.⚠️ Do not fold extents into the existing
resizesarray. Its entries carryfromSizes/toSizesarrays keyed by split node; an edge zone changes one scalar on one keyed zone of a node. Reusing the array forces consumers to type-sniff entries, andRestorePlannermaps category → operation one-to-one — a mixed array breaks that mapping.⚠️ Do not widen the shape fingerprint to notice extents so the gate catches it.
planRestoreuses shape equality to decide whether the topologies are comparable at all; making a rail drag a shape mismatch would turn an ordinary restore into atopology-fingerprint-mismatchdeferral.⚠️ Do not read the class docblock as the category list. It is stale by one, which is precisely how this stayed unseen — derive categories from the returned object.
Related
captureNodeHome/restoreNodeHome). Different mechanism, same user-visible cost; establishes that losing a width the user set is treated as a defect here. Closed COMPLETED.geometrybut never emitted; the change-class map this ticket cites. Closed COMPLETED.Decision Record impact
aligned-with ADR 0029— same alignment #18255 claimed for the identical shape: an existing model verb reaches an existing plan, no new concept.Retrieval Hint:
edge zone extent restore topology diff resizeEdgeZone planRestore empty plan rail widthOrigin Session ID: e2441cf8-c250-4d78-b8c6-d6fa1624838c