LearnNewsExamplesServices
Frontmatter
id18274
titleA dock commit builds a hidden stand-in per item and destroys it unused
stateClosed
labels
enhancementaiperformancedashboard
assigneesneo-opus-vega
createdAtSep 4, 2026, 3:30 PM
updatedAtSep 4, 2026, 4:32 PM
githubUrlhttps://github.com/neomjs/neo/issues/18274
authorneo-opus-vega
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtSep 4, 2026, 4:32 PM

A dock commit builds a hidden stand-in per item and destroys it unused

Closed v13.2.0/archive-v13-2-0-chunk-19 enhancementaiperformancedashboard
neo-opus-vega
neo-opus-vega commented on Sep 4, 2026, 3:30 PM

Context

Measured 2026-09-04 at dev@205bc52f8a while checking D#18150's "header object permanence" step before it became a ticket (receipt: https://github.com/neomjs/neo/discussions/18150#discussioncomment-18289767). Header chrome is already instance-stable across every commit class. What every commit does destroy is a set of components that never reach the DOM.

Live latest-open sweep: checked the latest 20 open issues at 2026-09-04T13:28Z; no equivalent found (#18208 is the Workstation's override of the item-only refresh; #18236, closed not-planned, was the comparator premise). A2A claim sweep over the last 30 messages: no competing claim; lane claimed 13:27Z.

The Problem

Unit tier, a two-node document with three items, every component.Base construct and destroy counted with its creator stack:

commit change class components destroyed of which stand-ins
setActiveItem (a tab click) topology 8 3
setItemLocked itemFlags 3 3
addTab topology 9 4
closeItem topology 10 3
setActiveItem on the already active item (a no-op) topology 8 3

The stand-ins are one Neo.create({module: Component, header, hidden: true}) per document item per commit, created hidden, never mounted, then destroyed. On the itemFlags path they are the whole cost. A consumer with twelve items pays twelve constructions and destructions per lock click, for a DOM delta of one class.

The Architectural Reality

  • Workspace#refreshDockWorkspace hands projectDockModel an item resolver that constructs a stand-in for every item (src/dashboard/dock/Workspace.mjs:3796–3802) through createProjectionPlaceholder (:1562–1568); the adapter calls it from LayoutAdapter.projectItem (src/dashboard/dock/projection/LayoutAdapter.mjs:915–930) via context.resolveComponentRef(componentRef, item, itemId), without the node id.
  • For a retained tab node the reconciler discards the whole projected tabs config behind a geometry placeholder (Reconciler.prepareTabChromeProjection, src/dashboard/dock/projection/Reconciler.mjs:271–312), so the stand-ins built for its items never enter a parent. The staged path then retires them (:407–420): "Pane placeholders inside a discarded config for a retained tab node never enter a parent. Retire them now; only genuinely new tab nodes need projected placeholders for pairing." The stable path retires them at :252–255. Both sweeps are the reconciler's contract towards any caller that hands it a full map — DockProjectionReconciler.spec.mjs:379–436 asserts it directly — so they stay; on the workspace path they become no-ops.
  • reconcileTabChrome treats a missing stand-in as a normal state: resolve(itemId) prefers the live pane (:720–726) and materialises a new one only when nothing is live (:764–814). Retained nodes reach that branch on every commit today, because their stand-ins were already destroyed.
  • Stand-ins ARE load-bearing for a new tab node: the staged container is built from the config, and the stand-in's index pairs the projected tab button with the live pane that replaces it (:774–789).

The Fix

Construct a stand-in only for an item whose tabs node is not retained — a caller-side change; the reconciler is untouched.

  • LayoutAdapter.projectTabsNode passes its nodeId into projectItem, which passes it as a fourth argument to context.resolveComponentRef.
  • refreshDockWorkspace collects the current shell's tab node ids (Reconciler.collectProjectedTabs(currentShell)) before projecting; its resolver returns null for an item in a retained node, so projectItem falls through to the item's persisted blueprint when present, otherwise to createPlaceholder(itemId, item) — a config either way, nothing constructed — which the reconciler discards with the tabs config as before.
  • Docblocks of createProjectionPlaceholder, projectItem and the resolveComponentRef option say when a stand-in exists and why.

Contract Ledger Matrix

Target surface Source of authority Proposed behavior Fallback Docs Evidence
LayoutAdapter.project({resolveComponentRef}) callback LayoutAdapter.projectItem (src/dashboard/dock/projection/LayoutAdapter.mjs:915–930) Called as (componentRef, item, itemId, nodeId); nodeId is the tabs node the item projects into and is undefined on the rail-reveal path. A 1–3-argument consumer is unchanged: the extra argument is ignored. A null answer falls through to item.blueprint when present, otherwise to createPlaceholder(itemId, item) — neither constructs an instance. Today's behaviour for every existing consumer; Workspace#resolveProjectedPane still answers the default resolver LayoutAdapter.mjs option and parameter JSDoc; Workspace#createProjectionPlaceholder docblock test/playwright/unit/dashboard/DockProjectionStandIns.spec.mjs: zero stand-ins for retained nodes across four commit classes; exactly one for the single item of a splitNode-minted node
Workspace#createProjectionPlaceholder(itemId, item, componentRef) Workspace#refreshDockWorkspace item resolver (src/dashboard/dock/Workspace.mjs:3796–3812) Invoked only for an item inside a tabs node the current shell does not render; a retained node's items never reach it. Signature and return unchanged. An override still shapes every stand-in that IS created createProjectionPlaceholder docblock same spec — the lock arm asserts zero invocations, the split arm exactly one

Acceptance Criteria

  • AC-1 — setItemLocked on a two-node fixture destroys zero components (today: 3), measured through a component.Base destroy counter in a unit arm.
  • AC-2 — setActiveItem, addTab and closeItem construct zero stand-ins for items whose tabs node is retained; createProjectionPlaceholder is called exactly once per item of a new tab node (a splitNode that mints one), so pairing in reconcileTabChrome is unchanged.
  • AC-3 — the header chrome census holds: tab container, header toolbar, tab buttons and action buttons keep their instance ids across all five commit classes above.
  • AC-4 — the reconciler is not modified; the source delta is the resolver predicate, the node id parameter and their docs, with no forwarding layer or second code path.
  • AC-5 — no regression against the exact base: DockWorkspace, DockLockAction, DockProjectionReconciler and DockLayoutAdapter unit suites stay green; every examples/dashboard/crossWindow and Workstation dock journey arm that is green on the base commit is green on the branch, and any arm red on the base is recorded with a durable owner rather than claimed green.

Out of Scope

  • The staged shell itself (edge-zone and split containers, the splitter, the two projection placeholders) built on every topology commit — that is retainTopology as the default admission request and waits on D#18224's rail reconcile decision (cut 2 on D#18150).
  • The reconciler's placeholder sweeps: they are its contract towards direct callers and stay.
  • Any change to toolbar.Base#syncActions or headerActions handling: header permanence is already true and measured.
  • The tear-out cluster.

Avoided Traps

  • Skipping stand-ins for items that merely have a live pane. A live pane moving into a new node still needs the stand-in for button pairing (:774–789); the retention of the NODE is the correct predicate, and the reconciler's own comment names it.
  • Deleting the reconciler's retire sweeps as "dead code". They are dead on the workspace path after this change, but DockProjectionReconciler.spec.mjs:432 asserts them for a caller that hands in a full map; deleting them changes a static utility's contract for one caller's convenience.
  • Fixing the comparator. #18236 tried to make isEqual treat functions by identity and was closed not-planned; the churn here is constructed instances, not change detection.

Decision Record impact

aligned-with ADR 0029 — same-instance projection is the ADR's rule; this removes constructions that never became instances of anything visible. No schema or wire change.

Related

Origin Session ID: 4384828f-2650-4d28-b650-fac55eebec59

Retrieval Hint: dock projection stand-in per item per commit createProjectionPlaceholder retained tab node churn census