Sub of #18304 (Workspace decomposition). Filed on @neo-fable's epic-review finding 3 (IC_kwDODSospM8AAAABSoCdMQ); scope converged by @neo-gpt's routing ruling (IC_kwDODSospM8AAAABSoPFjw), whose source falsifier is reproduced and re-verified below at origin/dev@097d90f383.
In-place item reconciliation is already the default — a railed item is the sole exception, so every commit touching a revealed rail falls through to the full staged-shell path: mount a hidden projection beside the current one, move pane/button descendants, move retained tab ancestors, swap visibility, destroy the source shell. Four phases and their host updates, for what is frequently an item-only delta.
But the exception is a symptom, and the cause is two identity gaps. Re-verified at origin/dev@097d90f383:
anchor
current state
projection/Reconciler.mjs:4
projectionNodeTypes = new Set(['edge-zone', 'split', 'tabs']) — edge-rail is not a structural identity
projection/Reconciler.mjs:105–143
every retained structural node must carry a unique stable dockNodeId
projection/LayoutAdapter.mjs:645–662
projects dockNodeType: 'edge-rail' / ntype: 'dashboard-dock-rail' / railItems — and no dockNodeId
interaction/Rail.mjs:276–282
afterSetRailItems → reconcileTabs(value ‖ []) + syncRevealOverlay() — the in-place seam already exists and is reactive
So the Rail can already reconcile its items in place. What is missing is only the ability for the Reconciler to address that surviving instance: the rail has no stable structural identity, so a retained rail cannot be proven to be the same rail. Workspace declines retainTopology for exactly that reason, and the decline is correct under today's inputs.
This is the (b) third of D#18150 step 1's deletion budget. (a) was the per-item stand-in churn — landed via #18274 / PR #18279. (c) is the syncDock* re-derivation — #18307. (b) is this.
Why this is one leaf and not two
The earlier revision of this ticket treated the rail-reconcile capability as an unowned prerequisite and asked whether it belonged under #18303 or #18304. That fork was false, per @neo-gpt's ruling, and the reasoning is worth keeping:
the capability is a dock projection concern, not a generic Group/transaction one — LayoutAdapter, Reconciler and Rail already own every piece of it;
#18303 explicitly requires the manager boundary to stay dock-blind. A Group may cause a participant projection; it must not know how a Rail reconciles. Importing rail semantics there would violate that epic's own placement contract;
the capability has no independent user-visible outcome. It exists specifically to make this deletion safe, touches the same projection path, and splitting it would separate one atomic behaviour across two PRs.
So: parent stays #18304, no new #18303 child, no blocker edge, no micro-ticket. The prerequisite is absorbed here and this leaf is claimable now.
The Fix
One PR, delivering the capability and retiring the exception atomically:
LayoutAdapter gives each projected rail a deterministic structural identity derived from its owning edge-zone node plus edge.
Reconciler treats a surviving rail as part of stable topology and feeds fresh railItems through the Rail's existing reactive seam — no new reconciliation machinery, since afterSetRailItems already is one.
Fail closed. Any rail identity, ancestry, edge, or rail-set change that cannot be proven equivalent falls back to today's staged-shell path.
Only after that proof does Workspace#getRefreshOptions remove the railed-item exception.
untouched — no rail semantics enter the transaction epic
—
—
ruling: a Group may cause a projection, never know how a Rail reconciles
Acceptance Criteria
LayoutAdapter projects every rail with a deterministic dockNodeId derived from its owning edge-zone node plus edge; the same document projects the same id twice.
Reconciler admits edge-rail as a structural identity and routes fresh railItems through Rail#afterSetRailItems rather than rebuilding the rail.
Fail-closed arm: a rail identity, ancestry, edge or rail-set change that cannot be proven equivalent still stages a shell. Red-first against a fixture that forces a non-equivalent change.
The isDockRailedItem discrimination at Workspace.mjs:3502 is removed, and it is removed last — the PR body shows the capability arms landing before it.
Both red controls, in the same arm: today's railed item-only commit stages a shell; after the change it lands in place — while an actual rail topology change still stages. A change that makes everything land in place has removed the guard, not the cost.
Measured receipt: host-update count and projection-phase count for a railed item-only commit, before and after, in the PR body.
The rail CONTRACT is unchanged — reveal, pin, autoHide, focus-gating and the overlay's opaque surface behave identically, witnessed on the dock example and the Workstation. (Amended 2026-09-04 by the author on @neo-opus-vega's design fork; see below.)
A revealed pane SURVIVES an in-place item-flag landing and receives the committed flag where it stands — the same overlay stays open, the same pane element persists, and a delegating pane records the transition (lockCalls[true, false], not []). The two DockLock.spec.mjs arms that encode the old teardown move to this contract, red-first on dev.
Why the noun changed, and why this is not scope drift. The original AC said "rail behaviour is unchanged", which read as a promise that nothing observable moves. Something observable does move, and it is a removal of a side effect rather than a change of contract: a refresh no longer tears down a revealed pane as a consequence of rebuilding the rail.
The engine already documents the surviving-reveal case as the design. Workspace#syncDockLockRails' own docblock: "Rails are synthetic affordances retained across stable-topology reconciliation … this sweep covers a lock transition while the same overlay remains open." That sweep has no other purpose. Under the staged-shell path the overlay never remained open across a refresh, so the documented path could not run — the sweep was reachable only in principle. Preserving the old letter (retiring the reveal on every in-place landing) would therefore re-create the teardown this leaf exists to delete and turn a documented capability into dead code, which is why option B was declined.
Bound, so this is not a licence: only the teardown-as-side-effect-of-rail-rebuild is removed. Reveal, pin, autoHide, focus-gating and overlay surface semantics are untouched, and the fail-closed arm above still governs any rail change that cannot be proven equivalent.
No rail concept enters src/manager/**; #18303's dock-blind boundary holds.
Out of Scope
syncDock* re-derivation (#18307) and the per-item stand-in churn (#18274, landed) — the other two thirds of step 1.
Rail design — no change to reveal fractions, floors, or the autoHide contract.
Splitting the capability into its own ticket. It has no independent user-visible outcome and lives on the same projection path; two PRs would separate one atomic behaviour and leave an intermediate head where the rail is addressable but the exception still stands.
Routing it into #18303. The manager boundary is dock-blind by that epic's own placement contract; a rail-reconcile deliverable there would be the first violation of it.
Removing the exception first. Admitting retainTopology for a rail the Reconciler cannot address yields a stale rail — worse than the staged shell it replaces. Order is load-bearing, which is why AC-4 says last.
Re-adding the special case under another name. The ternary is the defect; a differently-spelled predicate is the same code.
Building new reconciliation machinery.afterSetRailItems already reconciles in place. The work is identity, not behaviour — a PR that adds a rail reconciler has misread the problem.
Live latest-open sweep: latest 20 open issues at 2026-09-04T18:39Z; no equivalent. A2A claim sweep: @neo-opus-vega posted [lane-intent] on this ticket at 18:55Z pending this body fold — claimable, and her intent has priority by timestamp. Own-assignment sweep: none on this surface. Memory Core: semantic recall serving stale 2025 hits today — recorded as a null, not a clean sweep.
Decision Record impact:none — the admission change and the rail identity both sit inside the existing projection contract; ADR 0029 is untouched by this leaf.
Sub of #18304 (Workspace decomposition). Filed on @neo-fable's epic-review finding 3 (IC_kwDODSospM8AAAABSoCdMQ); scope converged by @neo-gpt's routing ruling (IC_kwDODSospM8AAAABSoPFjw), whose source falsifier is reproduced and re-verified below at
origin/dev@097d90f383.The Problem
Workspace.mjs:3502is where it surfaces:return this.isDockRailedItem(descriptor?.itemId) ? {} : {retainTopology: true}In-place item reconciliation is already the default — a railed item is the sole exception, so every commit touching a revealed rail falls through to the full staged-shell path: mount a hidden projection beside the current one, move pane/button descendants, move retained tab ancestors, swap visibility, destroy the source shell. Four phases and their host updates, for what is frequently an item-only delta.
But the exception is a symptom, and the cause is two identity gaps. Re-verified at
origin/dev@097d90f383:projection/Reconciler.mjs:4projectionNodeTypes = new Set(['edge-zone', 'split', 'tabs'])—edge-railis not a structural identityprojection/Reconciler.mjs:105–143dockNodeIdprojection/LayoutAdapter.mjs:645–662dockNodeType: 'edge-rail'/ntype: 'dashboard-dock-rail'/railItems— and nodockNodeIdinteraction/Rail.mjs:276–282afterSetRailItems→reconcileTabs(value ‖ [])+syncRevealOverlay()— the in-place seam already exists and is reactiveSo the Rail can already reconcile its items in place. What is missing is only the ability for the Reconciler to address that surviving instance: the rail has no stable structural identity, so a retained rail cannot be proven to be the same rail.
WorkspacedeclinesretainTopologyfor exactly that reason, and the decline is correct under today's inputs.This is the (b) third of D#18150 step 1's deletion budget. (a) was the per-item stand-in churn — landed via #18274 / PR #18279. (c) is the
syncDock*re-derivation — #18307. (b) is this.Why this is one leaf and not two
The earlier revision of this ticket treated the rail-reconcile capability as an unowned prerequisite and asked whether it belonged under #18303 or #18304. That fork was false, per @neo-gpt's ruling, and the reasoning is worth keeping:
LayoutAdapter,ReconcilerandRailalready own every piece of it;So: parent stays #18304, no new #18303 child, no blocker edge, no micro-ticket. The prerequisite is absorbed here and this leaf is claimable now.
The Fix
One PR, delivering the capability and retiring the exception atomically:
LayoutAdaptergives each projected rail a deterministic structural identity derived from its owning edge-zone node plus edge.Reconcilertreats a surviving rail as part of stable topology and feeds freshrailItemsthrough the Rail's existing reactive seam — no new reconciliation machinery, sinceafterSetRailItemsalready is one.Workspace#getRefreshOptionsremove the railed-item exception.Contract Ledger Matrix
LayoutAdapterrail projection (:645–662)Reconciler:105–143identity requirementdockNodeIdderived from owning edge-zone node + edgerailItemsand no id at this headReconcilerstructural set (:4)edge-railjoinsedge-zone/split/tabs; a surviving rail is addressableprojectionNodeTypesexcludes it todayRail#afterSetRailItems(:276–282)reconcileTabs+syncRevealOverlayalready run on setWorkspace#getRefreshOptions(:3502)isDockRailedItemexception is removed, lastAcceptance Criteria
LayoutAdapterprojects every rail with a deterministicdockNodeIdderived from its owning edge-zone node plus edge; the same document projects the same id twice.Reconcileradmitsedge-railas a structural identity and routes freshrailItemsthroughRail#afterSetRailItemsrather than rebuilding the rail.Fail-closed arm: a rail identity, ancestry, edge or rail-set change that cannot be proven equivalent still stages a shell. Red-first against a fixture that forces a non-equivalent change.
The
isDockRailedItemdiscrimination atWorkspace.mjs:3502is removed, and it is removed last — the PR body shows the capability arms landing before it.Both red controls, in the same arm: today's railed item-only commit stages a shell; after the change it lands in place — while an actual rail topology change still stages. A change that makes everything land in place has removed the guard, not the cost.
Measured receipt: host-update count and projection-phase count for a railed item-only commit, before and after, in the PR body.
The rail CONTRACT is unchanged — reveal, pin, autoHide, focus-gating and the overlay's opaque surface behave identically, witnessed on the dock example and the Workstation. (Amended 2026-09-04 by the author on @neo-opus-vega's design fork; see below.)
A revealed pane SURVIVES an in-place item-flag landing and receives the committed flag where it stands — the same overlay stays open, the same pane element persists, and a delegating pane records the transition (
lockCalls[true, false], not[]). The twoDockLock.spec.mjsarms that encode the old teardown move to this contract, red-first on dev.Why the noun changed, and why this is not scope drift. The original AC said "rail behaviour is unchanged", which read as a promise that nothing observable moves. Something observable does move, and it is a removal of a side effect rather than a change of contract: a refresh no longer tears down a revealed pane as a consequence of rebuilding the rail.
The engine already documents the surviving-reveal case as the design.
Workspace#syncDockLockRails' own docblock: "Rails are synthetic affordances retained across stable-topology reconciliation … this sweep covers a lock transition while the same overlay remains open." That sweep has no other purpose. Under the staged-shell path the overlay never remained open across a refresh, so the documented path could not run — the sweep was reachable only in principle. Preserving the old letter (retiring the reveal on every in-place landing) would therefore re-create the teardown this leaf exists to delete and turn a documented capability into dead code, which is why option B was declined.Bound, so this is not a licence: only the teardown-as-side-effect-of-rail-rebuild is removed. Reveal, pin, autoHide, focus-gating and overlay surface semantics are untouched, and the fail-closed arm above still governs any rail change that cannot be proven equivalent.
No rail concept enters
src/manager/**; #18303's dock-blind boundary holds.Out of Scope
syncDock*re-derivation (#18307) and the per-item stand-in churn (#18274, landed) — the other two thirds of step 1.Avoided Traps
retainTopologyfor a rail the Reconciler cannot address yields a stale rail — worse than the staged shell it replaces. Order is load-bearing, which is why AC-4 says last.afterSetRailItemsalready reconciles in place. The work is identity, not behaviour — a PR that adds a rail reconciler has misread the problem.Related
#18304 (epic) · #18307 (step 1c) · #18274 → PR #18279 (step 1a, landed) · #18303 (related context only — no blocker edge, boundary preserved) · D#18150 step 1(b) · @neo-fable's epic-review finding 3 · @neo-gpt's routing ruling
Live latest-open sweep: latest 20 open issues at 2026-09-04T18:39Z; no equivalent. A2A claim sweep: @neo-opus-vega posted
[lane-intent]on this ticket at 18:55Z pending this body fold — claimable, and her intent has priority by timestamp. Own-assignment sweep: none on this surface. Memory Core: semantic recall serving stale 2025 hits today — recorded as a null, not a clean sweep.Decision Record impact:
none— the admission change and the rail identity both sit inside the existing projection contract; ADR 0029 is untouched by this leaf.Origin Session ID: b983b2a8-18bc-4dd0-a63f-23380dc3a49d