Sub of #18151. Filed from a sighting during #18240 rather than fixed there — it is a design change, not a relocation.
Context
src/toolbar has no action class. An action is a plain config object stamped isToolbarAction: true (toolbar/Base.mjs:411-450), identified by a .action string, instantiated as whatever module its config names. actionMap defaults to null and nothing in src/ populates it. Membership is a boolean flag; there is no type.
So "what does locked mean for this action" has nowhere to live, and each action's state→presentation derivation — icon, accessible name, tooltip, visibility — is written outside the action, by every layer that needs it.
The Problem
The derivation is duplicated by construction, not by carelessness: once in the projection for the boot config, once in the runtime sync, in different layers.
projection/LayoutAdapter.mjs:1120-1131 (boot):
iconCls : context.items[activeItemId]?.locked === true ? context.dockUnlockIconCls : context.dockLockIconCls,
showOnFocus: context.items[activeItemId]?.locked !== true,
vdom : {'aria-label': context.items[activeItemId]?.locked === true ? 'unlock' : 'lock'},
...tip(context.items[activeItemId]?.locked === true ? 'unlock' : 'lock')dock/plugin/Lock.mjs#syncAction (runtime) computes the same four values from the same predicate.
Measured at origin/dev@274b63beac, each with the command that produced it:
| Measure |
Command |
Count |
?.locked === true in src/dashboard/dock/ |
git grep -o "?\.locked === true" 274b63beac -- src/dashboard/dock/ | wc -l |
11 |
any locked === true in src/dashboard/dock/ |
git grep -o "locked === true" 274b63beac -- src/dashboard/dock/ | wc -l |
14 |
'unlock' : 'lock' derivations in src/dashboard/ |
git grep -c "'unlock' : 'lock'" 274b63beac -- src/dashboard/ |
4 |
'restore' : 'maximize' derivations in src/dashboard/ |
git grep -c "'restore' : 'maximize'" 274b63beac -- src/dashboard/ |
2 |
Six independent derivations of two toggle mappings, and nothing structural holding any pair equal. One reader cannot be promised; it has to be a type.
The Architectural Reality
This is the generator behind #18151's measured clusters rather than another instance of them. Because an action cannot own its own derivation:
- the projection writes it (boot),
- every host sync writes it again (runtime),
- and a host that wants a different action writes both again.
That is the syncDock*Action cluster (761 LOC / 16 methods in a 4,317-line class), and it is why apps/workstation carries 1,022 lines of tab-header chrome — 23.7% of a file nobody can navigate. The missing type is the multiplier.
The Fix
An action class that owns its state→presentation mapping. The projection emits identity plus state — {action: 'lock', pressed} — and the action derives icon, accessible name and tooltip in its own afterSet; the focus gate derives from pressed at the toolbar, which owns contextual presence. Boot and runtime become one code path, so drift stops being possible instead of being documented as forbidden.
Placement follows the in-package precedent (tab/plugin/Overflow.mjs beside the class it extends); toolbar/ActionButton.mjs extending button/Base.mjs is the shape to argue about, and the argument belongs in D#18150 alongside the vehicle question.
Contract Ledger Matrix
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback |
Docs |
Evidence |
| toolbar action config |
toolbar/Base.mjs:411 |
gains a class that owns derivation |
plain-config actions keep working |
class JSDoc |
no class exists today |
toolbar/Base#isFocusGatedAction |
:331 |
derives the effective gate from pressed; the declared showOnFocus is never rewritten |
ungated actions unchanged |
method JSDoc |
the projection derived the gate from item state |
LayoutAdapter header-action configs |
:1120-1131 |
emit identity + state, not derived presentation |
— |
— |
the four-read block above |
dock/plugin/Lock.mjs#syncAction |
#18240 |
writes one state config |
— |
— |
the same quad, second copy |
Workspace#syncDockMaximizeActionPresentation |
Workspace.mjs |
same collapse |
— |
— |
second 'restore' : 'maximize' derivation |
Decision Record impact
aligned-with ADR 0029. No new docking concept — this gives an existing one an owner.
Acceptance Criteria
Out of Scope
- The vehicle question (B vs E) — D#18150's, and orthogonal: this is what an action owns, not where the concern lives.
- Migrating hosts that supply their own actions. The plain-config path stays.
apps/workstation's 1,022 lines. They shrink as a consequence, in their own ticket.
- Pop-out. It is not a toggle: its two expressions mirror a visibility predicate (
dockPopOutActionActive), which an action class does not address. Its cannot drift comment at Workspace.mjs:1763 therefore stays, and so does model/Document.mjs:68, whose subject is the persistence-vs-projection rejection boundary.
Avoided Traps
- Fixing the duplication by deleting one copy. Both exist for a reason — boot needs a config, runtime needs a write. Removing either breaks the other; only an owner collapses them.
- Reading this as a lint.
?.locked === true eleven times is the symptom. Counting it without giving the action an owner produces an eleventh-and-a-half copy.
- Treating it as dock-specific.
toolbar/Base is engine-wide; the dock is where the absence became measurable.
- Counting a grep instead of reading its hits. This ticket's first AC-4 asked for three
cannot drift comments to be deleted, on the assumption that all three promised a toggle's copies equal. None of them does. See the correction comment below.
Related
- Parent: #18151 — this is the generator behind its measured clusters.
- #18240 — the PR that surfaced it; it relocates one copy and leaves the duplication intact, by design.
- D#18150 — the 4,317-line decomposition; this changes what its clusters weigh.
defect-note captured 2026-09-04T00:04Z under ticket-create §1e.
Live latest-open sweep: latest 20 open issues at 2026-09-04T00:05Z; no equivalent. A2A sweep: no competing claim.
Origin Session ID: e8ea32a2-b9a8-43ea-84ba-b83081ec4b3d
Retrieval Hint: "toolbar action has no class derivation duplicated projection and sync cannot drift comment locked === true focus gate derives from pressed"
Sub of #18151. Filed from a sighting during #18240 rather than fixed there — it is a design change, not a relocation.
Context
src/toolbarhas no action class. An action is a plain config object stampedisToolbarAction: true(toolbar/Base.mjs:411-450), identified by a.actionstring, instantiated as whatevermoduleits config names.actionMapdefaults tonulland nothing insrc/populates it. Membership is a boolean flag; there is no type.So "what does locked mean for this action" has nowhere to live, and each action's state→presentation derivation — icon, accessible name, tooltip, visibility — is written outside the action, by every layer that needs it.
The Problem
The derivation is duplicated by construction, not by carelessness: once in the projection for the boot config, once in the runtime sync, in different layers.
projection/LayoutAdapter.mjs:1120-1131(boot):iconCls : context.items[activeItemId]?.locked === true ? context.dockUnlockIconCls : context.dockLockIconCls, showOnFocus: context.items[activeItemId]?.locked !== true, vdom : {'aria-label': context.items[activeItemId]?.locked === true ? 'unlock' : 'lock'}, ...tip(context.items[activeItemId]?.locked === true ? 'unlock' : 'lock')dock/plugin/Lock.mjs#syncAction(runtime) computes the same four values from the same predicate.Measured at
origin/dev@274b63beac, each with the command that produced it:?.locked === trueinsrc/dashboard/dock/git grep -o "?\.locked === true" 274b63beac -- src/dashboard/dock/ | wc -llocked === trueinsrc/dashboard/dock/git grep -o "locked === true" 274b63beac -- src/dashboard/dock/ | wc -l'unlock' : 'lock'derivations insrc/dashboard/git grep -c "'unlock' : 'lock'" 274b63beac -- src/dashboard/'restore' : 'maximize'derivations insrc/dashboard/git grep -c "'restore' : 'maximize'" 274b63beac -- src/dashboard/Six independent derivations of two toggle mappings, and nothing structural holding any pair equal. One reader cannot be promised; it has to be a type.
The Architectural Reality
This is the generator behind #18151's measured clusters rather than another instance of them. Because an action cannot own its own derivation:
That is the
syncDock*Actioncluster (761 LOC / 16 methods in a 4,317-line class), and it is whyapps/workstationcarries 1,022 lines of tab-header chrome — 23.7% of a file nobody can navigate. The missing type is the multiplier.The Fix
An action class that owns its state→presentation mapping. The projection emits identity plus state —
{action: 'lock', pressed}— and the action derives icon, accessible name and tooltip in its ownafterSet; the focus gate derives frompressedat the toolbar, which owns contextual presence. Boot and runtime become one code path, so drift stops being possible instead of being documented as forbidden.Placement follows the in-package precedent (
tab/plugin/Overflow.mjsbeside the class it extends);toolbar/ActionButton.mjsextendingbutton/Base.mjsis the shape to argue about, and the argument belongs in D#18150 alongside the vehicle question.Contract Ledger Matrix
toolbar/Base.mjs:411toolbar/Base#isFocusGatedAction:331pressed; the declaredshowOnFocusis never rewrittenLayoutAdapterheader-action configs:1120-1131dock/plugin/Lock.mjs#syncActionWorkspace#syncDockMaximizeActionPresentationWorkspace.mjs'restore' : 'maximize'derivationDecision Record impact
aligned-with ADR 0029. No new docking concept — this gives an existing one an owner.Acceptance Criteria
LayoutAdapter's header-action configs carry state, not derived presentation. The four-read block at:1120-1131is gone.?.locked === truereads insrc/dashboard/dock/drop from 11, and any-formlocked === truefrom 14; the PR body states both numbers with the commands that produced them.'unlock' : 'lock'drops from 4 to 0 and'restore' : 'maximize'from 2 to 0 insrc/dashboard/, so no comment is left promising two copies stay equal — there is one copy.Out of Scope
apps/workstation's 1,022 lines. They shrink as a consequence, in their own ticket.dockPopOutActionActive), which an action class does not address. Itscannot driftcomment atWorkspace.mjs:1763therefore stays, and so doesmodel/Document.mjs:68, whose subject is the persistence-vs-projection rejection boundary.Avoided Traps
?.locked === trueeleven times is the symptom. Counting it without giving the action an owner produces an eleventh-and-a-half copy.toolbar/Baseis engine-wide; the dock is where the absence became measurable.cannot driftcomments to be deleted, on the assumption that all three promised a toggle's copies equal. None of them does. See the correction comment below.Related
defect-notecaptured 2026-09-04T00:04Z underticket-create §1e.Live latest-open sweep: latest 20 open issues at 2026-09-04T00:05Z; no equivalent. A2A sweep: no competing claim.
Origin Session ID: e8ea32a2-b9a8-43ea-84ba-b83081ec4b3d
Retrieval Hint: "toolbar action has no class derivation duplicated projection and sync cannot drift comment locked === true focus gate derives from pressed"