LearnNewsExamplesServices
Frontmatter
id18206
titleThe engine declares resize as geometry but never emits it, so every consumer hand-writes the fast path
stateClosed
labels
enhancementaiarchitecturedashboard
assigneesneo-opus-grace
createdAtSep 3, 2026, 3:11 PM
updatedAtSep 3, 2026, 4:28 PM
githubUrlhttps://github.com/neomjs/neo/issues/18206
authorneo-opus-grace
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtSep 3, 2026, 4:28 PM

The engine declares resize as geometry but never emits it, so every consumer hand-writes the fast path

Closed Backlog/active-chunk-20 enhancementaiarchitecturedashboard
neo-opus-grace
neo-opus-grace commented on Sep 3, 2026, 3:11 PM

Context

Epic #18151 row 1. #18152 landed the derived default: getRefreshOptions now answers from Operations.operationChangeClass instead of returning {}, so a lock toggle stopped restaging the shell. It wired one of the three declared classes and said so in the code:

geometry is declared honestly in the class map but deliberately NOT emitted yet: on dev a resize takes the full transaction, this ticket measured only the lock flicker, and its AC-4 requires resize to keep its current behaviour. Putting every drag-resize on an unmeasured fast path is a separate change with its own evidence, not a free rider on this one.

This is that separate change, with that evidence.

The gap

operationChangeClass declares resizeSplit: 'geometry' and resizeEdgeZone: 'geometry'. The engine never emits it, so getRefreshOptions returns {} for both and every drag-resize in a consumer that writes no hook takes the full staged transaction — the same restage #18152 removed for lock.

One host does not pay that, because it hand-writes the mapping (apps/workstation/view/Workspace.mjs:2215):

geometryOnly: geometryOnly === true || ['resizeEdgeZone', 'resizeSplit'].includes(operation),

That list is a verbatim restatement of what operationChangeClass already declares. It is the epic's thesis in one line: the host re-derives knowledge only the engine has, and a consumer that does not know to write it gets the slow path silently.

Evidence — the class is measured, not asserted

The map's own doc sets the standard ("verifiable rather than asserted"), so I applied it. Both reducers, run through the real Document.commit (which is normalizeTree + validate, the path that could restructure behind a reducer's back):

resizeSplit resizeEdgeZone
errors empty ✅ ✅
the write actually happened ✅ [0.5,0.5] → [0.8,0.2] ✅ 0.25 → 0.4
node tree identical ✅ ✅
node id set identical ✅ ✅
items byte-identical ✅ ✅

The non-vacuity gate matters here. My first probe reported "node tree identical: true" for an operation that had errored and returned the document untouched — a refusal reading as a pass. Every row above is therefore conditioned on the write having landed. The instrument is also armed: the same comparison run over moveItem detects the change, so the greens are discriminating rather than blind.

Source agrees with the measurement — each reducer clones and writes exactly one field (nodes[id].sizes, nodes[id].zones[edge].extent); every other line is a refusal.

Why the blast radius is smaller than it looks

geometryOnly is only an admission request. Reconciler.reconcileStableTopology returns null on any node/type/ancestry/order/orientation delta and falls through to the staged transaction, and landedInPlace reports the path actually taken (#18188). So emitting geometry cannot produce a wrong projection even if the class map were wrong — the worst case is one wasted validation pass. The measurement above is what makes it a fast path; the reconciler is what makes it a safe one.

Proposed change

  1. getRefreshOptions returns {geometryOnly: true} for the geometry class, beside the existing itemFlags branch.
  2. Delete the now-engine-owned clause from apps/workstation's override. Its two remaining jobs are genuinely its own — the geometryOnly === true passthrough for its options-object commit shape, and its preserveItemIds merge — and they stay.
  3. Witness the default in a fixture that writes no getRefreshOptions, the way DockLock.spec.mjs witnesses the lock path.

Acceptance Criteria

  • getRefreshOptions emits {geometryOnly: true} for resizeSplit and resizeEdgeZone, derived from the change class and not from an operation-name list.
  • A red-first unit arm fails on the current default and passes after: a geometry operation on a workspace with no override returns geometryOnly.
  • A component arm witnesses the outcome, not the request: a resize on a hook-free fixture reports landedInPlace: true. Reverting the change turns it red.
  • apps/workstation's override no longer names resizeSplit/resizeEdgeZone, and its own resize behaviour is unchanged — measured, not assumed.
  • The deferral comment in getRefreshOptions is replaced by what is now true, including which class remains unemitted and why.

Non-goals

  • detachItem / transferNode. apps/workstation requests retainTopology for both while the map declares them topology. That is not a contradiction — the class map is deliberately conservative and retainTopology is a different axis (items may change, structure holds) — but the host clearly knows something the engine does not, and moving that knowledge needs its own measurement. Separate lane.
  • Changing the reconciler's validation, or the topology default for unmapped operations.

Related

#18151 (epic, row 1) · #18152 (the derived default; this is the successor its code names) · #18188 (landedInPlace as the outcome witness this AC leans on) · #18176 (restoreTab's class)

Sweep before filing: live latest-20 open (no equivalent), getRefreshOptions and operationChangeClass full-text over open+closed (#18152 closed, no successor filed), own open issues.

Authored by Grace (Claude Opus 5, Claude Code). Session 8d936ec9-b816-4ded-a91e-6e91ef6a3325.