TWO work products share this commit (a staged-index collision between the
coordinator's docs commit and the OP7 fixer's staged files — content
verified complete and coherent; only this message was wrong before the
amend):
1. OP7 review fixes (all nine findings from
docs/research/2026-08-11-op7-review.md):
- M1: HeadlessSessionDescriptor is a record; WithAccount uses 'with' non-destructive record copy,
so a future property cannot be silently dropped; direct-CLI
regression test proves CharacterOptions survives --user/--password.
- M2 root fix: LiveSessionEventRouter skips BOTH Replace and the
options notification on a trailer-truncated PlayerDescription — a
truncated re-seed can no longer install zeroed words under an armed
latch for OP7's automation to flush into 0x01A1.
- SF1: schema keys validate as ordinal strings against the allowed
names (numeric / comma-combined aliases rejected). SF2: both-true
fellowship exclusion rejected at load, naming both keys. SF3: the
onLoginCompleteSent observer moved after transit.EndTeleport().
SF4: production-hook coverage for all three LoginComplete sites.
SF5: test-script OP7 wire expectation corrected (batched ids ride
only the 0x01A1).
2. docs/research/2026-08-11-op3-rereview.md — OP3 re-review verdict
REOPEN (narrow): M1 byte-decode independently re-verified (6a 07 at
all six sites); residuals R1 (gate script promises a timestamp prefix
acdream doesn't render), R2 (null-controller player-mode still
refuses), R3 (dormancy pin lacks stimulus) — coordinator fixes follow.
Full Release suite at this tree: 12,956 passed / 4 skipped / 0 failed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
24 KiB
Campaign OP slice OP3 — fix-round RE-REVIEW (closure check)
Commit under review: 386076af ("fix(ui): OP3 review fixes — byte-verified
Magic chat lines, Gameplay/OptionPage shape, mid-air tri-state, shared
geometry")
Claims closure of: every finding in
docs/research/2026-08-11-op3-review-mechanism.md (M1, S1–S6, N1–N8) and
docs/research/2026-08-11-op3-review-blast.md (MUST-FIX 1/2, SHOULD-FIX 1–4,
NOTE 1–8).
Method: read-only. No build, no test run, no client launch. Git reads and
a raw byte read of the PDB-paired retail binary only.
VERDICT: REOPEN (narrow)
The substance of every finding is closed correctly — including M1, whose byte-level claim I re-derived independently and confirmed exactly. Three residuals remain, all cheap, none requiring a design change:
| ID | Class | One-line |
|---|---|---|
| R1 | MUST-FIX | The fix round's own "timestamped, logged" claim is FALSE for acdream (no timestamp prefix is rendered anywhere; no chat-log-file writer exists). It is repeated in the gate script's step 15, so the connected gate now instructs the user to look for something that cannot appear. |
| R2 | SHOULD-FIX | The tri-state IsGrounded does not implement the third state it documents: IsPlayerMode == true with a null controller still returns false → mid-air refusal. Three comments and the commit message assert the opposite — the same comment-vs-code inversion S3 was filed to remove. |
| R3 | SHOULD-FIX | The new dormant-ActivePageChanged pin applies no stimulus, so it asserts silence in a vacuum. It passes identically if dormancy is completely broken. |
Everything else is genuinely closed. Details below, evidence first.
0. What I verified as CORRECTLY CLOSED
0.1 M1 — INDEPENDENTLY BYTE-VERIFIED, claim exact
The commit claims 6a 07 at 0x0049E972/E9E2/EA52/EAA4/EAF6/EB48.
Internal consistency (the check I was asked for). The mechanism review
recorded the six AddTextToScroll call addresses as 0x0049E97C/E9EC/
EA5C/EAAE/EB00/EB52. Every one of the commit's six offsets is exactly
10 bytes below its matching call — a uniform delta, and the first one
(0049e972) is verbatim the BN line the review quoted. Re-reading
docs/research/named-retail/acclient_2013_pseudo_c.txt:169512-169640 confirms
all six *(uint32_t*)((char*)var_2c)[4] = 7; lines carry precisely those six
addresses. The recorded evidence is complete and self-consistent.
Independent decode (beyond what was asked; the binary read is read-only).
C:\Users\erikn\Downloads\acclient.exe — TimeDateStamp 0x52291F34 =
2013-09-06 00:17:56 UTC, the PDB-paired build; ImageBase 0x400000, .text
VA 0x1000 / raw 0x1000, so VA→file offset is identity minus base. Bytes at
each of the six sites, with six bytes of lead-in:
6A 00 push 0 ; arg5 windowId = 0
6A 01 push 1 ; arg4 allowPluginFilter = 1
8D 57 14 lea edx,[edi+14]
6A 07 push 7 ; arg3 type = 7 <-- THE CLAIM
52 push edx ; arg2 text
E8 16 56 0C 00 call 0x00563F90 ; ClientUISystem::GetUISystem
8B C8 mov ecx,eax ; this
E8 3F 96 FE FF call 0x00487FC0 ; ClientSystem::AddTextToScroll(char const*)
All six sites are byte-identical in shape (6A 00 / 6A 01 / 8D {57,4F,47} 14 / 6A 07 / {52,51,50} / call / mov ecx,eax / call). Both call targets resolve by
name in the pseudo-C: :368536 00563f90 ClientUISystem::GetUISystem() and
:148528 00487fc0 ClientSystem::AddTextToScroll(ClientSystem* this @ ecx, char const* arg2, uint32_t arg3, uint8_t arg4, uint32_t arg5). arg3 (type)
is 7 at all six sites. Confirmed.
The code change matches the evidence. RetailUiRuntime.cs:1945-1946 routes
the six macro lines through the new
OptionsRuntimeBindings.DisplayMouseTurningMacroLine, bound at
InteractionRetainedUiComposition.cs:832-833 to
AddText(text, RetailLogTextType.Magic); RetailLogTextType.Magic = 0x07
(src/AcDream.Core/Chat/RetailLogTextType.cs:33). The mid-air refusal
(RetailUiRuntime.cs:1905) and UA/RA (RetailUiRuntime.cs:1982 →
OptionsPanelController.cs:189-192) both still use DisplaySystemMessage =
ClientLocal. Exactly the split the review asked for.
Route traced to the chat scroll, no truncation.
RuntimeCommunicationState.AddText (:195-203): Trim(), then
if (type == ClientLocal) { SpewBox.Enqueue; return; } — Magic falls through
to Chat.OnSystemMessage(text, 7). ChatLog.OnSystemMessage (:266-293)
appends a ChatEntry with LogTextType = 7; Append (:414-418) trims only
against _maxEntries (default 500, :42), so six lines cannot be dropped
— the 4-slot SpewBoxState.MaxConcurrentItems truncation that discarded
Camera Stiffness and Camera Adjustment is gone. The main window's default
type filter is 0xFBFFFFFF (ChatWindowState.cs:111), which clears only bit
0x1A; bit 7 passes. The SystemDedupWindow short-circuit cannot collapse
the burst — all six strings are distinct
(MouseTurningSettingsMacro.cs:58-104). Colour is #3FBFFF light blue
(2026-08-09-chat-retail-color-table.md:151), matching what the gate script
tells the user to expect. ✅
(The one thing about this route that is NOT true is the "timestamped, logged" half — see R1.)
0.2 S1 — Gameplay's slot carries no AfterApply; the other three keep theirs
OptionsPanelController.cs:138 _pages.Add(GameplayPageId, new OptionPage { AfterApply = null }); and :139-143 the loop that still gives
Character/Chat/Config AfterApply = afterApply. The seam OP4-6 will bind
survived intact: Callbacks.AfterApply (:81) → RetailUiRuntime.cs:1983-1984
→ Publish(new SaveCharacterOptionsRuntimeCmd()), unchanged, and the three
real pages are constructed from it in the same constructor. Nothing about
OP4-6's wiring path was narrowed.
Keeping an (empty) OptionPage for the Gameplay slot is one of the two fixes
the review itself offered ("or construct it with AfterApply = null and
document why"). OnShown()→Apply() over zero rows is then: empty
foreach, AfterApply?.Invoke() null, OnOptionChanged?.Invoke() null —
byte-for-byte the no-op retail's plain UIElement::OnVisibilityChanged is.
The two tests were rewritten to the right shape.
OptionsPanelControllerTests.cs:63-83 ActivateTabs_SelectsGameplayAsDefault_ ButNeverFlushesIt now asserts gameplayFlushCount == 0 with the controller's
own AfterApply wired — a real pin, not a rename. :143-159
GameplayPage_OnShownAndOnHidden_NeverFlush_EvenWhenControllerAfterApplyIsWired
pins the same property independent of tab framing. The two
OptionPageModelTests entries were retargeted at a synthetic empty page and
renamed to say so, with the class doc (:5-22) now explicitly stating neither
shape models the Gameplay tab.
The model's doc no longer overclaims. OptionPageModel.cs:157-172 keeps
the generic empty-page property but bolds "This is a property of the generic
empty-page shape, not a description of the Gameplay tab" and cites
acclient.h:55857 UIElement_Field. ✅
0.3 S2 — OnOptionChanged fires at retail's four call sites, Defaults ungated
OptionPage.Apply (:222-228) → rows, AfterApply, then OnOptionChanged
(retail 0x004F2C95). Reset (:236-241) → changed rows, then
OnOptionChanged (0x004F2D4A). Defaults (:246-251) → all rows, then
OnOptionChanged (0x004F2CE5). Per-row live edit: BoolOptionRow. SetCurrentValue (:100-105) notifies; RestoreSavedValue/
RestoreDefaultValue deliberately do not — retail's Apply(0) paths, where
the owning verb notifies once itself. Wired by OptionPage.Register (:211)
through the new IOptionRow.AttachPageNotify. This is exactly the shape the
review specified.
Defaults is never gated: the model contains no gate at all, and the
OnOptionChanged doc (:191-203) records that retail's own override never
fetches 0x100001FE. OnShown/OnHidden inherit the notify through
Apply/Reset, matching retail's OnVisibilityChanged → SaveCurrentValues/
RestoreSavedValues, both of which end in OnOptionChanged.
Usable by OP4-6. The seam is Action? OnOptionChanged + the existing
bool Changed (:217); a page binds
page.OnOptionChanged = () => { apply.Enabled = page.Changed; reset.Enabled = page.Changed; }, which is precisely retail's GetChildRecursive(0x100001FC/FD)
SetState(0x01/0x0D). The model correctly does NOT own the enabled state itself. Six new tests pin order (["afterApply","onOptionChanged"]), all three verbs, the empty-page case, theSetCurrentValue-only row notify, theRestore*non-notify, andRegister's attachment. ✅
0.4 S4/S5/S6 + blast MUST-FIX 2 — register and script bookkeeping
- AD-77 (
retail-divergence-register.md:174) is scoped client-wide ("everyMount*/RegisterMainPanelcall site for agmPanelUIsibling … client-wide, not an Options-specific gap"), classed AD not TS, carries the five-column shape with retail anchors, and states the inherited-not- introduced nuance the review asked for. Section count bumped 56 → 57 in the same hunk (:65); TS count correctly not bumped (TS-74 amended, not added). No retired row resurrected in the diff. - TS-74 (
:408) now names all five store-only keys in What/Where (Camera_Stiffness,Camera_AdjustmentSpeed,Camera_AlignToSlope,Input_MouseLookSensitivity,Input_InvertMouseLookYAxis), citesCameraTurningSettings.csandCameraPointerInputController.csas the two separate stores, and adds the "chat line quotes an unrelated stored value" consequence to Risk. Exactly S5's ask. - Test script: step 3 gains the ghosted→enabled + highlight check; step 5
is corrected to bottom-edge-only with an explicit "this is a correction from
OP3-as-landed" note; steps 13/14 gain the UA/RA legibility eye-item (blast
SHOULD-FIX 4); steps 16/17 add relogin (
0x0005survived) and relaunch (five prefs survived), both written so a failure is discriminating rather than ambiguous; "What to report" updated to 17 steps. ✅
0.5 blast SHOULD-FIX 2 — geometry now matches the cohort exactly
RetailUiRuntime.cs:2021-2023 ResizeX = false, ResizeY = true, ResizableEdges = ResizeEdges.Bottom, with MinWidth/MinHeight removed. I
read every sibling that calls RegisterMainPanel: spellbook (:1337-1339),
effects (:1528-1530), the four indicator-detail panels (:1751-1753),
character (:2266-2268), inventory (:2349-2351) — all identical. No
ResizeX/min-size divergence remains. ContentAnchors all-four matches
effects and the indicator-detail panels, so it is not a new outlier either. ✅
0.6 Blast radius OF the fixes — clean on all three axes
- Typed message seam:
OptionsRuntimeBindingshas exactly ONE construction site (InteractionRetainedUiComposition.cs:801) andDisplayMouseTurningMacroLineexactly ONE consumer (RetailUiRuntime.cs:1946). No test constructs the record, so the added positional parameter breaks nothing. Func<bool>→Func<bool?>: one production caller (RetailUiRuntime.cs:1899); theswitchcoverstrue/false/nullexhaustively. The unrelatedIsGroundedoccurrences across Runtime/Core.Net areUpdatePosition.PositionFlags.IsGrounded— a different symbol entirely.IOptionRow.AttachPageNotify:BoolOptionRowis the ONLY implementer (no test fakes), so widening the interface compiles everywhere.- Geometry: no test pins the old Options geometry. The only Options
geometry in tests is the new
RetailPanelUiControllerTestshelper mount. A stale persisted width from the pre-fix build is also harmless — I traced it:RetailWindowLayoutPersistence.Apply:213→RetailWindowHandle.ResizeTo:69-70→RetailWindowManager.ResizeTo:184if (!frame.ResizeX) width = frame.Width;. The saved width is discarded on restore. Verified non-issue.
0.7 Two of the three new pins genuinely pin
RetailPanelUiControllerTests.cs:131-148uses the REALRetailPanelCatalog.Options(10) /.Character(11) ids against a realRetailPanelUiController.RegisterMainPanel, and asserts the show-hides-the- other transition in both directions plusActivePanelId. Genuine — it would fail ifSetPanelVisibility's exclusion regressed.RetailDialogFactoryTests.cs:95-110exercises the queueing end-to-end (ActiveCount == 1,PendingCount == 1), which really would break to2/0if the omitted-key overload took a different key. Slightly tautological at the language level (the default ISDefaultQueueKeyin the signature,RetailDialogFactory.cs:128), but the behavioural assertion is real.- The third does not pin — see R3.
1. RESIDUALS
R1 — MUST-FIX: "timestamped, logged" is retail's behaviour, not acdream's, and the gate script now tells the user to look for it
The claim, in three places:
src/AcDream.App/UI/RetailUiRuntime.cs:190-192(XML doc onDisplayMouseTurningMacroLine): "Magic routes throughRuntimeCommunicationState.AddTextto the scrolling chat transcript (light blue, timestamped, logged)".- The commit message: "routed at Magic (scrolling chat transcript, light blue, timestamped)".
docs/research/2026-08-11-campaign-op-test-script.md, step 15: "expect LIGHT BLUE text in the SCROLLING CHAT TRANSCRIPT (not the transient SpewBox overlay), with a timestamp prefix, same as any other Magic-colored system line."
acdream renders neither. Evidence:
ChatEntry.Receivedexists (src/AcDream.Core/Chat/ChatLog.cs:457) but has zero consumers —git grep "\.Received" -- src/returns onlyNetClient.cs/FragmentAssembler.cshits on unrelatedReceivedBytes/ReceivedCountmembers. Nothing formats it into a line.ChatSettings.ShowTimestamps(src/AcDream.UI.Abstractions/Panels/Settings/ChatSettings.cs:46) is a stored preference whose only references are its own declaration, its default (:107), the unrendered ImGui-eraSettingsPanel.cs:415-417, andSettingsStore.cs:196/597. No renderer reads it.- There is no chat-log-file writer at all:
git grep -ni "chatlog\.txt|WriteChatLog|LogChatToFile" -- src/returns nothing, and register row TS-69 records@logas unported precisely because "needs a safely-lifecycled chat-to-file writer (deferred)".
Why MUST. Item 3 corrupts the acceptance instrument this whole fix round exists to make meaningful. The blast review's own MUST-FIX rationale was that M1 "changes what the gate is supposed to observe"; a gate step that asks the user to confirm a prefix acdream cannot draw will produce either a false failure report or, worse, a follow-up "fix" for a non-bug. Item 1 is the same class of defect S1 just retired in this very commit: a doc sentence naming an acdream type and method while describing retail's mechanism.
Fix (documentation only, no code): in step 15 drop "with a timestamp
prefix"; in RetailUiRuntime.cs:190-192 either drop "timestamped, logged" or
re-attribute it ("retail additionally timestamps and log-files this type;
acdream does neither — see TS-69"). Nothing about the Magic routing itself
changes.
R2 — SHOULD-FIX: the tri-state does not implement its own third state
src/AcDream.App/Composition/InteractionRetainedUiComposition.cs:814-816:
IsGrounded: () => !d.PlayerMode.IsPlayerMode
? (bool?)null
: d.PlayerController.Controller is { IsAirborne: false },
Enumerating the states as asked:
| State | Expression | RequestExitToCharacterSelection |
Retail | ✓ |
|---|---|---|---|---|
| player mode, controller live, grounded | true |
EndCharacterSession() |
proceed | ✅ |
| player mode, controller live, airborne | false |
"Cannot log off while in mid-air." | refuse | ✅ |
| not player mode | null |
silent | silent | ✅ |
player mode, controller null |
false |
refusal | silent | ❌ |
The fourth row is the miss. Controller is { IsAirborne: false } is false
for a null controller, so it lands in case false: (RetailUiRuntime.cs:1904-1906)
and emits the refusal.
Three comments assert the opposite of the code — the exact inversion S3 was filed to remove:
InteractionRetainedUiComposition.cs:807-808: "with no player object (not in player mode, or no live controller yet) neither the refusal nor the logoff itself ever fires, i.e. a SILENT no-op, not a refusal."RetailUiRuntime.cs:161-163: "nullmeans 'no player' (acdream:!d.PlayerMode.IsPlayerMode || d.PlayerController.Controller is null)".RetailUiRuntime.cs:1908-1911: "outside player mode (or with no live controller) there is no player object forUseTimeto test at all".
Plus the commit message's own "(or with no live controller)".
Reachability is narrow but the two owners are explicitly independent.
IsPlayerMode is App-owned and set true only after the controller is in hand
(PlayerModeController.cs:370, after controller.Position is read at :362),
and Exit() clears it at :166. But PlayerModeController.cs:167-169 states
outright: "the movement controller is Runtime-owned — player-mode exit
detaches presentation only; the publication lifecycle (generation reset/
teardown) owns the controller's retirement." Two owners, two clocks. Any
ordering in which Runtime clears the controller before App clears
IsPlayerMode reaches row four.
Fix (one expression):
IsGrounded: () => !d.PlayerMode.IsPlayerMode
|| d.PlayerController.Controller is null
? (bool?)null
: d.PlayerController.Controller is { IsAirborne: false },
which is literally what the XML doc already says.
On the "no dedicated automated test" flag: for the switch itself,
acceptable — RetailUiRuntime is composition-driven and a direct unit test
would need a UiRoot plus DAT layouts, and there are zero test constructions of
OptionsRuntimeBindings today. But note what the absence cost here: an
untested expression silently failed to implement its own documented
contract. If R2 is fixed, the cheapest real pin is a one-line test over the
lambda shape rather than the runtime — or simply leaving it, given the fix
makes expression and doc agree by construction.
R3 — SHOULD-FIX: the dormant-ActivePageChanged pin applies no stimulus
tests/AcDream.App.Tests/UI/Layout/OP2ReworkBlastRadiusConformanceTests.cs:83-93:
var host = Assert.IsType<UiTabPanel>(layout.FindElement(0x100000B8u));
int raised = 0;
host.ActivePageChanged += (_, _) => raised++;
Assert.False(host.BehaviorActive);
Assert.Equal(0, raised);
Nothing between the subscription and the assertion does anything. The test
would pass identically if the dormancy model were entirely broken, because no
switch is ever attempted. It adds only the subscription over the test directly
above it (:61-70), which already asserts BehaviorActive == false and
ActivePageElementId == 0.
The parent's framing is right and the answer is the weaker variant shipped.
Worth being precise about what stimulus is correct, because "call SwitchTo
and assert silence" would legitimately fail: UiTabPanel.SwitchTo has no
dormancy guard and raises unconditionally at its tail
(src/AcDream.App/UI/UiTabPanel.cs, ActivePageChanged?.Invoke(...) as the
last statement). Dormancy lives entirely in the click bindings:
ActivateTabBehavior is the only caller of
RetailTabBinding.SetClick(button, () => SwitchTo(pageId)), and
RetailTabBinding.SetClick (RetailTabBinding.cs:12-23) sets
button.OnClick = action.
Fix: drive the authored tab button on a dormant host —
var button = layout.FindElement(host.Tabs[0].ButtonElementId);
Assert.Null(((UiButton)button!).OnClick); // never wired -> the real mechanism
Assert.Equal(0, raised);
Assert.Equal(0u, host.ActivePageElementId);
That fails the moment eager wiring returns, which is the regression AD-73 exists to prevent.
2. NOTES (no action required)
- N1 —
EmptyPage_WithNoAfterApplyWired_Apply_NeverFlushes(OptionPageModelTests.cs:90-101) asserts onlyAssert.Null(thrown). The name promises "never flushes"; withAfterApplynull there is nothing to count, so it is really a null-safety test. Harmless because the real S1 pin lives inOptionsPanelControllerTests.cs:143-159and does count. Rename or leave. - N2 — plan §5 still open-ended.
docs/plans/2026-08-10-options-panel-campaign.md:381still reads "register row in OP3 if the review deems it a divergence" with no pointer to AD-77. A future reader re-litigates a settled ruling. One-line edit whenever that file is next touched. - N3 —
BoolOptionRow.AttachPageNotifyis a single-slot assignment (OptionPageModel.cs:107). Registering one row into two pages silently drops the first page's notify. This matches retail's singleUIOption::m_pOCHpointer, so it is faithful, not a bug — recorded so OP4-6 does not share a row instance across pages expecting both to hear it. - N4 — Options can now only GROW. With
MinWidth/MinHeightremoved and noDatConstraintSourcepassed,RetailWindowFrame.ResolveConstraint:187-201falls through to thefallbackargument, which is the mounted outer extent (:161-164) — i.e. min = the authored 300 x 362 plus chrome inset. That is identical to all nine siblings, so it is the correct outcome, but gate-script step 5 says only "resizable from the bottom edge only … remembers its HEIGHT" without noting the window cannot be dragged shorter than it opens. A tester may report that as a defect. One clause would pre-empt it. - N5 —
OptionsPanelController.Pages' doc (:104-109) still describes the four slots uniformly ("Every entry exists from construction … only GameplayPageId's stays permanently empty") without mentioning theAfterApplyasymmetry. The constructor comment immediately below covers it. Cosmetic. - N6 — test counts unverified. The commit's "12,947 passed / 4 skipped / 0 failed" was not re-run; this re-review is read-only by instruction.
3. Closure table
| Finding | Claimed | Actually |
|---|---|---|
| M1 (both lenses) | closed, byte-verified | CLOSED — independently re-decoded; 6a 07 at all six sites, arg3 of ClientSystem::AddTextToScroll @0x00487FC0. Route traced to the chat scroll, filter passes, no truncation, no dedupe collapse. R1 is a doc/gate overclaim attached to it, not a routing error. |
| S1 | closed | CLOSED — Gameplay slot AfterApply = null; other three keep the real seam; both tests rewritten to the right shape; model doc corrected. |
| S2 | closed | CLOSED — notify at all three verb tails + SetCurrentValue only; Defaults ungated; seam usable by OP4-6. |
| S3 / blast SHOULD-FIX 1 | closed | PARTIAL → R2. Three of four states right; IsPlayerMode && Controller == null still refuses, and three comments plus the commit message say it does not. |
| S4 / blast MUST-FIX 2 | closed | CLOSED — AD-77 filed, client-wide, AD-classed, count 56→57, format correct. |
| S5 | closed | CLOSED — TS-74 What/Where names all five keys and both stores. |
| S6 | closed | CLOSED — all three script additions present and discriminating (but see R1 on step 15). |
| blast SHOULD-FIX 2 (geometry) | closed | CLOSED — matches all nine siblings exactly; stale persisted width provably discarded on restore. |
| blast SHOULD-FIX 3 (three pins) | closed | 2 of 3 CLOSED → R3. Mutual exclusion and queue key genuinely pin; dormant ActivePageChanged does not. |
| blast SHOULD-FIX 4 (UA/RA legibility) | closed | CLOSED — eye-item added to steps 13/14 with the honest-fix direction named. |
| blast NOTE 8 (close-button logging) | closed | CLOSED — OptionsPanelController.cs:176-179, symmetric with BindButton. |
| New blast radius from the fixes | — | CLEAN — one construction site for the new seam, one caller for the changed signature, one IOptionRow implementer, no test pins the old geometry. |