Opus dual-lens review of cb6502c8 passed with six findings; this lands
the fix round:
1. headless-portability.yml: AcDream.Platform src/tests join both path
triggers and the presentation-free build/test arrays — the moved XDG
tests run on ubuntu-latest again (they had fallen out of every Linux
lane).
2. acdream-architecture.md: AcDream.Platform gets its own layer block;
Runtime may-reference clause updated (the guard changed in cb6502c8,
its human-readable twin had not).
3. PlatformDependencyBoundaryTests: the BCL-only contract (zero
project/package references) is now enforced, not just observed.
4. memory/project_linux_graphical.md canonical seam renamed.
5. Plan LA0 recon corrected: the K0 Headless guard was never the guard
needing amendment (it asserts Headless own refs); Runtime own-refs
guard was — the commit did the right thing, the plan text now says so.
6. App declares its AcDream.Platform reference explicitly per its own
convention instead of riding transitivity.
Platform.Tests: 4 passed (3 moved + the new guard).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
User decision 2026-08-14: everything the launcher does ships Linux-tested
in this campaign (launcher UI, install/update with manual DAT picker,
headless launches with plugins + login commands, probe, per-slice Linux
test runs, Linux connected-gate section at LA11). GUI client launches
stay Windows-only until Slice L resumes later; the launcher renders GUI
modes disabled on Linux with an explicit note, and the host-agnostic
session-config contract means Slice L lights them up with no launcher
changes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Plan doc with twelve slices, dependencies, review protocol (Opus
dual-lens: architectural + retail-faithful), and ledger. Three parallel
recon reports grounded the slice bodies:
- Retail select screen is gmCharacterManagementUI: flat listbox +
Enter/Delete/Restore + dialogs. NO 3D preview (that machinery is
chargen-only gmCG3DView) — the spec 3D-preview slice is deleted, the
old retail-ui/05-panels.md pedestal claim is uncited and wrong.
Restore + CharacterError join scope; delete sends account+slot.
- Chat-command core (parser/router/catalog/ChatVM) is dependency-clean
BCL+Core; extraction to Runtime is a move, not a rewrite.
- Probe reuses the NoCharacters early-exit shape (graceful teardown at
the CharacterList stage exists today); roster plumbing is new.
- Bake tool needs --progress-json + explicit --out; no whole-file SHA
exists — launcher records/verifies its own.
- UI Studio is deleted (Campaign V) — stale references corrected.
Roadmap + CLAUDE.md Current state carry the campaign pointer.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The two-client user gate PASSED 2026-08-14 (open both ways, stage with
the retail trading marker, accept/decline, executed swap, Clear All,
cancel text). Every TEMPORARY [trade] probe line from gate rounds 1-2 is
stripped (ItemInteractionController, SelectionInteractionController,
SecureTradeUiController, LiveSessionCommandRouter, WorldSession,
RuntimeTradeState). Roadmap gains the shipped-trade ledger row.
Suites after strip: App 4,992/3, Runtime 1,626 - green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The retail four-tab social panel (Fellowship & Allegiance) is
code-complete: all six slices landed and reviewed (dual-lens Opus review
-> fix round -> narrow re-review each). Fellowship two-session flow proven
live (FA6 bot gate PASSED). Closeout bookkeeping:
- register AD count 66 -> 67 (AD-87, the deferred allegiance bot gate);
- plan status flipped to CODE-COMPLETE with the OWED connected gates +
#384 (allegiance-swear ACE non-response) called out;
- CLAUDE.md Current-state gains the Campaign FA paragraph
(per feedback_claude_md_staleness), pointing at the memory digest.
Owed: the user's connected gates (§FA3-§FA6 of
docs/research/2026-08-12-campaign-fa-test-script.md) and #384's
ACE-console disambiguation. Full suite 13,304/4/0.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Plan ledger: fellowship two-session automated gate PASSED live 2026-08-12
(five of six runs reproduced the decisive cross-session assertion); the
allegiance bot gate is DEFERRED behind AllegianceGateEnabled=false pending
docs/ISSUES.md #384, with commit citations for every fix this slice landed
(confirmation relay, name-matched proximity, the fellowship-only
finalization).
Gate script §FA6: the fellowship automated-gate recipe + actual PASSED
result (the two-session config, the six proof points per stage, the
literal decisive-assertion log lines), the allegiance deferral writeup,
and a new [TWO-CLIENT] manual step (25) the user's own connected gate can
run to help disambiguate #384 (ACE-side rule vs wire-builder defect vs
harness-specific drop) using two real graphical clients instead of the
testaccount/testaccount2 pair.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Register:
- AD-84 -- Swear button's missing "target is a player" enable-rule gate,
same class as AD-83's Recruit-button gap.
- AD-85 -- the unported StringInfo variable-substitution engine (AD-81's
same root cause) extended to the Allegiance page's numeric-only
followers/rank/experience-passed-up fields and its three local
confirmation dialogs (verbatim-or-bare-name, never invented).
- AD-86 -- ACE's deliberate zeroing of seven AllegianceProfile/
AllegianceData fields (officers, officer titles, MOTD, MOTD-set-by,
name-last-set-time, lock, approved vassal, timeOnline, allegianceAge),
dropped past acdream's own parse layer to match retail's own
gmAllegianceUI, which has no widget for any of them either.
- AD-82 addendum: the vassal-row click-target-only selection shares
point (3)'s limitation, but NOT the invented leader/selection tints
(point 1/2) or the Fellowship-only world-selection sync (point 4) --
Allegiance's list-selection message has no SetSelectedObject call.
Gate script: new docs/research/2026-08-12-campaign-fa-test-script.md
SFA5 section, mirroring SFA4's structure -- the CF-1 subscription steps
(including the reconnect-while-closed MF-3-REOPEN analogue), the SF-7
per-relationship monarch/patron steps, vassal-list steps, swear/break/
kick with their confirmations, the ACE-zeroed-field honesty note, and
full "what to report"/"explicitly not in scope" lists.
Plan ledger: FA5 row filled in against 7ed79eaf with per-item summary,
directly-measured totals (13,296/4/0, +11 net from FA4's 13,285/4/0),
and the two primary-source resolutions this slice needed beyond the
research docs (the self-rank field's live buffed-quality source, and
"your follower count" == _total_vassals, confirmed by a fresh targeted
decompile of UpdatePlayerData rather than inferred).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
FA4 row now names all six fix-round commits (290f9b58, 5499f058, df000306,
300d8189, 55b17e15, 1d743277) alongside the original landing's three, and
records: build/test green at every commit; the +13/0-deletion test delta
broken down per file; the final directly-measured 13,285 passed / 4
skipped / 0 failed (13,289 total); every MUST-FIX/SHOULD-FIX/NIT applied;
and the corrected dimmed-row arithmetic (35 -> 31 FA4-original -> 34
fix-round final, net one row). Also corrects item (4) of the
"contradictions/deferrals" list, which called the missing Recruit
is-a-player register row an acceptable inline comment -- MUST-FIX 5 named
that the wrong call; it is now register row AD-83.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Register (docs/architecture/retail-divergence-register.md):
- AD-78: the Character-tab dimmed count had drifted stale through two
campaigns (still read "35" after FA4 shipped 31; now 34 after the fix
round's three reversions). Addendum explains the full D6/SF-8 chain.
Blast review's own SHOULD-FIX 1.
- AD-82 (new): the invented leader-tint/selection-tint colors, the
name-text-only row click target, and the page-local (not generic
UiTemplateListBox) world->panel selection sync -- MUST-FIX 4's
disposition plus two items MUST-FIX 5 named as owed rows.
- AD-83 (new): the Recruit button's missing "target is a player" gate,
previously an inline comment, not a register row -- MUST-FIX 5's third
item. Section header bumped 61 -> 63 active rows.
Gate script (docs/research/2026-08-12-campaign-fa-test-script.md):
- SF-7: fixed step 3's self-contradiction ("only Quit" then "Disband and
Open should ALSO be enabled").
- MUST-FIX 3: new reconnect step after the existing close/reopen step.
- MUST-FIX 4: new world-selection step under the recruit/dismiss/quit
section.
- MUST-FIX 1: new HARD-check step for the 6/8-fellow 44%/34% truncation
(distinct from the existing SOFT 9-member ACE-divergence note).
- MUST-FIX 2 correction: the old invite steps tested whether acdream's
CLIENT gates the dialog on the option bits -- a mechanism that never
existed in retail and no longer exists in acdream. Rewritten to test
the corrected behavior (the dialog always shows regardless of the
target's own checkbox state) and to explain what ACE-side filtering
would look like if the local server implements it, so a tester doesn't
misattribute ACE's behavior to a client bug.
- Renumbered steps 9-22 to 9-25 to fit the two new steps; updated the
"what to report" section's step cross-references and rewrote its
invite/dimming bullets to match the corrected mechanism.
Plan (docs/plans/2026-08-11-fellowship-allegiance-campaign.md):
- D7 addendum: SF-8's further correction (FellowshipShareLoot reverts
too; only FellowshipShareXP survives as genuinely live).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
D6 asserted the client consumes IgnoreFellowshipRequests/
FellowshipAutoAcceptRequests on the invite path; the FA4 mechanism review
(913e35cd MUST-FIX 2) byte-verified retail reads NEITHER bit client-side
(Handle_Character__ConfirmationRequest @0x005640A0, RecvNotice_
FellowshipRequest @0x00490880, MakeFellowRequestDialog @0x00490620) — ACE
filters both server-side. Same class as the D2 reset-lifetime correction.
D6/D7 corrected in-place with dated addenda: the client-side intercept is
removed, the invite dialog always shows, and the two un-dims revert
(dimmed 31->33). D8 records the user-provided second account
(testaccount2/testpassword2) and the recruit-proximity requirement.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Register: AD-80 files the D5 XP-share display divergence between
retail's byte-decoded table (acdream renders it verbatim) and the
currently-targeted ACE server's slightly different actual grant (.3 vs
.3111111 at 9 fellows, no 10-fellow row, wrong out-of-range default) --
an ACE-vs-retail gap, not an acdream-vs-retail one, filed because it is
directly user-visible through this panel. AD-81 files the two unported
retail text-composition primitives the fellowship page's mechanism
needs (StringInfo variable substitution, ACCharGenData::FormatName) and
what acdream renders instead (plain numeric composites, the raw typed
name). AD-78's derivation table gains its D7 addendum: 4 of the 35
store-only rows (IgnoreFellowshipRequests/FellowshipAutoAcceptRequests/
FellowshipShareXP/FellowshipShareLoot) moved to the Live bullet with
their new consumers named.
Gate script: new §FA4 section covering create (name + shareXP), the
open/close caption swap, button-enable rules, and the D5 display -- all
solo-testable -- plus roster/recruit/dismiss/leader-handoff/invite-
dialog steps marked [TWO-CLIENT] with an honest note that they defer to
FA6's bot-vs-ACE gate if a second account isn't available for this
connected gate. Corrects FA3's now-stale "these six buttons/four
checkboxes are INERT" claims in steps 11-12 to point at the new
section instead of leaving a wrong claim in place.
Ledger: FA4 row CODE-COMPLETE with both commit SHAs, the reconciled
13,238->13,272 (+34) test-count arithmetic, the live-DAT verification
summary (ACDREAM_PROBE_LIVE_MOUNT=1 against real installed DATs,
including the structural finding that retail's own frame-visibility
swap already gates the Create-flow controls away from the roster view
with no extra code needed), and the four scoped
deferrals/simplifications this slice made (the StringInfo/FormatName
gap, the proportional-share omission, the Recruit button's
superset-of-retail enable rule).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Records the FA3 fix-round closure: both dual-lens reviews' findings
(mechanism 2 MUST-FIX/9 SHOULD-FIX, blast 1 MUST-FIX/6 SHOULD-FIX/1 NIT)
applied across four commits (9afa05b5/35c40a9b/ae772709/a5553904).
Baseline 13,233/4/0 (13,237 total) -> fix-round 13,238/4/0 (13,242
total), +5 tests, arithmetic verified directly against all 9 test
project totals. FA3 still owes the user's connected gate against the
now-corrected script.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mechanism SHOULD-FIX 2 / blast SHOULD-FIX 4 (same finding, both reviews):
SocialAllegiancePageController.Tick() closed over a local `lines` on
every call — `() => lines` allocated a display class plus a delegate on
EVERY frame, unconditionally, at ~2,000 allocations/second at the
profile's measured FPS, in a hot loop the Modern Runtime slices spent
whole commits driving to 0 B/frame. Hoisted two static readonly
Func<IReadOnlyList<UiText.Line>> providers (BlankLineProvider/
NoLinesProvider); Tick() now assigns the cached delegate reference —
zero allocation while idle or active.
Mechanism SHOULD-FIX 7: FA3's empty-state gate is coarser than the
retail mechanism it is contracted against — gmAllegianceUI::UpdateMonarchData
@0x00491B40 hides the monarch/patron blocks per-relationship (monarch
block also hides when the monarch IS the viewer; patron block on the
analogous test), while this shell gates both blocks on the single
HasProfile flag. Not a MUST-FIX for FA3 (MF-2 in the same review means
HasProfile is effectively always false for the whole FA3 gate, so
nothing wrong is visible during this slice's own gate) — recorded
instead as an explicit FA5 acceptance line in the plan's FA5 row so the
gap cannot be silently lost, plus a class-doc note that FA5's real
monarch/patron population must also change Tick()'s unconditional
LinesProvider reassignment in the same commit or its content will be
overwritten the next frame.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- docs/research/2026-08-12-campaign-fa-test-script.md: the user's
connected-gate script for FA3 -- open paths (F3/F4 + tab switch),
gmPanelUI exclusivity vs sibling panels, tab switching, all four
pages' expected shells/empty states, Friends/Squelch read-only
expectations + the D1 INERT buttons, what to report, what's
explicitly out of scope (FA4/FA5/FA6/D1's deferred wire).
- Plan ledger: FA3 row filled in with commit SHAs, totals (13,233/4/0,
13,237 total, +18 over FA2's close), the tab-table correction finding,
and the reverted unrelated fixture drift note. Campaign status line
updated from "FA3 in flight" to implementation-complete pending
review + gate.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The narrow re-review (cc1a319c) closed FA2 with no reopen; its
carry-forward corrected the FA5 row: the allegiance page's data
subscription is 0x001F AllegianceUpdateRequest(on) at retail's three
arming points, not the text-only 0x027B info request.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Plan (docs/plans/2026-08-11-fellowship-allegiance-campaign.md):
- D2 gets the D9-style dated strike/addendum recording the corrected
allegiance reset semantics (clears at every generation reset; the
HasServerSeed latch only gates pre-seed rendering WITHIN a session) with
the three-way evidence citation: the retail OnEndCharacterSession hook,
the RuntimeCharacterOptionsState precedent's actual clear-and-relatch
behavior, and the no-character-selector connect path
(SessionPlayerComposition.cs:1127).
- The architecture blurb and FA2's slice-map contract row get matching
strike/addendum corrections so the "fellowship session-scoped,
allegiance survives reconnect" claim does not survive uncorrected
anywhere in the plan.
- FA2's ledger row: fix-round commit SHAs, corrected delegate-hole/
wrapper counts (blast SHOULD-FIX 3: 10 not 15, 11 not 12), the
allegiance register-row re-evaluation conclusion (blast SHOULD-FIX 6 --
no row needed, MF-1's fix retires the deviation entirely), and the
reconciled fix-round test totals (13,201/4/0 -> 13,215/4/0, +14,
arithmetic exact per file).
Seam map (docs/research/2026-08-11-fa-acdream-seams.md), per the FA1
fix-round's established in-place-correction convention:
- SS1.3 and SS9's dispatcher-replaces-not-chains correction is now dated
and cites the actual GameEventDispatcher.Dispatch behavior, matching the
code comment already landed in GameEventWiring.cs.
- SS2.3 gets the 0x01C9/0x01CA disposition it was missing (correctly
left unregistered -- dead COMDAT-fold no-ops per FA1) so FA3 does not
have to re-derive it or "fix" the gap.
- The SS8 seam-map table's Allegiance-owner row and the executive-summary
ownership bullet both get the "survives reconnect" claim struck with a
dated correction to "session-scoped, clears at every generation reset".
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
TS-81 filed: 0x027A AllegianceLoginNotification's retail two-line chat
text is not emitted -- the candidate strings resolve through Binary-
Ninja-mislabeled symbols at gmAllegianceUI::RecvNotice_AllegianceLogin
(0x00492220) that need a DAT string-table lookup, not a guess.
TS-80 partially narrowed: the fellowship-create shareXp wire mechanism
now exists end-to-end (IRuntimeFellowshipCommands.Create takes and
sends it), but no caller reads the option bit yet -- that's FA4's
create-dialog scope.
Updates the campaign plan's FA2 ledger row: code-complete, full test
totals (13,158/4/0 -> 13,201/4/0, +43 exact), the seam-doc dispatcher
correction, and the entity-table-borrow recommendation that wasn't
needed (the wire's own FellowMember record already carries full vitals
inline).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The narrow re-review (96df892d) closed FA1 with no reopen and one
carry-forward: two further forward-looking seam-map rows (:128 owner
diagram, :214 state-parameter pattern) still cited the deleted
AllegianceTree as FA2 design guidance. Both now carry dated strike/
addendum notes pointing FA2 at the parsed profile records instead.
FA2 unblocked per the re-review's precondition.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Applies the documentation-only MUST-FIX items from the blast review
(docs/research/2026-08-12-fa1-review-blast.md), plus mechanism SF-4:
Blast MF-1 / mechanism SF-4: register row AP-90
(retail-divergence-register.md) cited the deleted AllegianceTree class as
its evidence. Re-pointed to ClientCommandResponses.AllegianceProfileLookups
and the fellowship parsers FA1 added -- the deviation itself (radar
relationship state undelivered at runtime) is unchanged and NOT retired,
since FA2 hasn't wired a live owner yet.
Blast MF-2: corrected four falsified statements in the lane D research doc
(fa-acdream-seams.md), each marked with a dated, clearly-struck FA1
fix-round addendum rather than silently rewritten (it is a committed
research record):
- :791 "wrapping existing AllegianceTree" -- class deleted; re-pointed to
AllegianceProfileLookups.
- :666/:672 `commands.Fellowship.SetOpen -> BuildFellowshipUpdate` -- that
builder no longer exists; its renamed successor is panel visibility,
not openness, and using it here would re-introduce the exact semantic
bug FA1 fixed. Re-pointed to BuildFellowshipChangeOpenness (0x0291).
- :429/:668 `BuildFellowshipCreate(seq, name, openness, shareXp)` -- the
builder is now 3-arg; there is no wire openness field.
- :854 open question 8 (trailing-pad rule) -- ANSWERED by FA1 (VC-3),
closed with the answer instead of left open for re-derivation.
Blast MF-3: plan decision D9 and the FA1 slice-map row both asserted "the 8
missing fellowship WeenieError strings are added in FA1" -- FA1 shipped the
opposite, verified finding (no retail display text exists for any of the
8 ids). Both struck and annotated with the actual outcome.
Blast MF-4: reconciled the ledger's internally-inconsistent test-total row.
Direct measurement at the pre-fix-round tip (bc693728, stashed/restored
during this session to isolate it) confirms 13,149 passed / 4 skipped / 0
failed (13,153 total) -- the ledger's own prior number was actually
correct; the "baseline 13,103" and "net +50" framing next to it did not
reconcile with each other or with the diff-verified delta (+58 added / -9
deleted = net +49, one test of drift attributed to a different baseline
commit, not a further miscount). Also records this session's own +9 tests
and the blast SF-1 live-surface note (FA1 changed observable @allegiance
info output, not a purely-unwired slice).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Records the FA1 commit SHAs and automated-gate test totals in Campaign
FA's ledger (§9). Owes its dual-lens review per the campaign's §7
protocol before FA2 (the RuntimeFellowshipState/RuntimeAllegianceState
owners) begins.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Seven slices over the four committed research lanes + the U2 slot-table
closure: Core.Net truth (repair/complete/delete the H.2 scaffolding),
two sibling J-owners with different lifetimes, the OP3-recipe mount of
the ONE four-tab social panel (id 12: Friends/Allegiance/Fellowship/
Squelch), the two live pages, bot-vs-ACE gates (second ACE account is
the user prerequisite at FA6), and closeout. Nine design decisions
stated (D1-D9); Campaign OP's gate lessons imported as binding rules.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The campaign's /goal stop condition is reached: all nine slices landed and
reviewed (OP1/OP2/OP7/OP9 CLOSED; OP3-OP6 + OP8 code-complete with
connected gates owed), and the gate script is the complete per-tab
connected-gate contract (launch with ACDREAM_RETAIL_UI=1; §OP7 already
PASSED live bot-vs-ACE). OP9's ledger row records the combined review
chain (289bf5bc APPROVE-WITH-FIXES -> residuals 07f2b3f7) including
SF-4's corrected test-delta arithmetic (-84, not the implementation
commit's '-80 exactly'). CLAUDE.md Current-state gains the campaign
paragraph per feedback_claude_md_staleness; the settings digest
(claude-memory/project_settings_options_digest.md) is the new domain
entry point.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Merge 1c5cd969 lands OP8 on the campaign tip after 057d8cd7, so the
#372 viewport fix covers OP8's six ListBoxes (the re-review's merge
precondition). Post-merge full Release suite: 13,155 / 4 skips / 0
failures. #373 captures the deferred DAT ActionMap.ConflictingMaps
consultation the OP8 round-2 conflict-universe fix scoped out.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ports retail's Configure Keyboard screen (gmKeyboardUI, LayoutDesc
0x21000009) — its own separate full-screen window, not a fifth Options-
panel tab. Retires OP3's INERT contract for the Gameplay tab's Configure
Keyboard button (0x10000204).
DAT reader (src/AcDream.Core/Input/RetailActionMap.cs): reads the
ActionMap singleton (DID 0x26000000, empirically the only one — not
0x27000000 as GetDBOType's Turbine-internal tag would suggest) and both
MasterInputMap defaults (0x14000000 "gmDefaultMap"/0x14000002
"DefaultMap"), union-merged per (InputMapId, ActionId) — proven order-
independent since the two maps' one shared context (0x5) has disjoint
action-id sets. Empirically resolved three lane-D unknowns against the
live DAT: the six ActionClass values (1=Movement, 2=Camera, 3=UI,
4=Combat, 5=Emote, 7=CharacterSettings — 6 is genuinely absent), that
the six unnamed InputMaps are 100% non-bindable (render nothing, not an
unlabeled group), and that the enum-to-DID pairing for the two master
maps is inconsequential to the merge result.
Identity table (src/AcDream.UI.Abstractions/Input/RetailActionIdentityTable.cs):
maps DAT (InputMapId, ActionId) pairs to acdream's InputAction where a
live consumer exists (~140 of 306 user-bindable rows — Movement/Camera/
Combat map almost completely; UI/Quickslot/Chat partially; only 5 of 87
Emotes and none of 48 CharacterSettings hotkeys, since acdream has no
general emote player or hotkey-to-option-toggle dispatcher yet). Every
entry cross-verified by label match AND a DAT-default-vs-
KeyBindings.RetailDefaults() byte comparison (RetailActionIdentityRoundTripTests),
which caught a real off-by-one in the Quickslot 13-18 block before it
shipped and found three genuine pre-existing RetailDefaults() gaps
(walk-mode's Shift-echoed chord, ten CameraAlternateControls arrow-key
alternates, and the Quickslot Ctrl+N use-vs-select ambiguity) — none
introduced by this slice, all documented rather than silently patched.
KeyboardConfigController: six ActionClass list boxes built from the
DAT, merged with live KeyBindings for mapped rows (rebind applies
immediately through the same InputDispatcher every other input path
uses) and a new sibling RetailUnmappedKeyBindings store for rows with
no InputAction yet. Left-click a key button opens real InputDispatcher
modal capture; right-click erases that slot. N-way conflict detection
scans every other row plus the live KeyBindings table for acdream-only
actions (Ctrl+M mute, debug F-keys) as the non-user-bindable refusal
analogue, using retail's own byte-verified "Could not overwrite "
string (table 0x23000004). OK/Cancel/Defaults/Revert reuse the
OptionPage/IOptionRow verb model via a new ActionKeyMapOptionRow.
Persistence is keybinds.json only (D4 — no .keymap file interchange).
Five register rows: AP-202 (.keymap interchange narrowing), AP-203
(store-only rows with no live consumer), AP-204 (silent auto-reassign
instead of retail's confirm dialog; OK/Cancel ported as left-click not
right-click-release).
Small supporting additions: UiButton.OnRightClick (additive, no
existing behavior changed), InputDispatcher.Bindings getter (the
screen's single live-truth read seam), RetailScanCodeMap (DIK scan
code <-> Silk.NET Key, keyboard + the one mouse-device row).
19 new tests (6 ActionMap reader conformance incl. live-DAT row-count/
label pins, 1 DAT-vs-RetailDefaults round-trip, 12 controller
behavior tests against the committed keyboard_config_21000009.json
fixture) — full solution suite 13,147 passed / 4 skipped / 0 failed
(baseline 13,128/4/0, zero regressions).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The re-reviewer byte-decoded ALL six caption sites independently (no
transposition; a third evidence line from the fixture's authored
left/right label geometry), re-verified the audio chain through the one
place an inversion could still hide (the checkbox pass-throughs), and
hand-traced the 97-key conformance table (exactly 97, none invented or
dropped). Residuals applied here: SF-1 the AudioSettings doc comment's
wrong function attribution (the SetDefaultValue literals live in
gmConfigUI::InitOptions @0x0049E435/E457/E479, not InitUIPreferences);
SF-2 the gate script no longer asserts an unread DAT caption — it gates
on behaviour and asks the tester to report the authored English
verbatim; lane A's stale '24 option rows' corrected to 27 (the 39-item
pin is the authoritative tally). AudioSettings.cs change is
comment-only (no executable-code delta; suite state carries from
67b0815c, re-verified at the next code commit).
Campaign state: OP1-OP7 ALL CODE-COMPLETE; OP3/OP4/OP5/OP6 gates ready;
OP8 (Configure Keyboard) is the sole remaining implementation slice.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Evidence appended to the test script: run-1 diff-and-send exact to
contract; mid-run reconnect idempotence; run-3 cross-process persistence
proof (the fresh seed echoed the blob-only SalvageMultiple value);
graceful converged exits; no refusals, no pre-LoginComplete sends.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Closes the mechanism lens's REOPEN (one MUST-FIX) and both lenses' small
residuals on the OP2 rework (b236a442); the blast lens re-review was
CLOSED outright. Fable-direct per the two-failure escalation rule.
- AP-195 filed: UIOption_CheckboxBitfield64 ports HALF of Refresh
@0x004859C0 — the ANY-set checkbox predicate is exact, but the ALL-set
LED media swap (P0x10000082=0x06004D17 / P0x10000083=0x06004D19) and
the ListBox self-sizing tail (ResizeTo/CalculatePaperSize — the block
IS a UIElement_ListBox in retail) are unported, and the block's row
stacking is a second divergent implementation beside UiTemplateListBox.
All due at OP5 before the Chat tab's connected gate; the IsSet doc
comment now names both halves instead of quoting only the ported one.
- Row tooltips: UiButton gains settable TooltipText surfaced through the
shared GetTooltipText hover pipeline (UiCatalogSlot's pattern);
UiCheckboxBitfield64.AddChild applies the row tooltip retail stamps in
CreateChildren @0x00485DF0, and documents that the 0x10000084 row-index
attribute stamp is deliberately replaced by the typed mask closure.
- AD-73 addendum: UiTemplateListBox.ConsumesDatChildren=true is inert
only while no authored Type-5 element carries children — that premise
is now conformance-PINNED across all 32 fixtures (a future DAT
regeneration surfacing an authored child fails the build instead of
silently dropping it).
- Plan doc: OP2's contract names UiTabPanel.cs (retail UIElement_Panel),
not the fictional-class-named UiTabControl.cs; ledger records OP1 and
OP2 both CLOSED.
Full Release suite: 12,871 passed / 4 skipped / 0 failed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Focused re-review verifies all ten fix dispositions genuinely close their
findings (fellowship order re-derived from OnChanged @0x0059A8E0; the
seed latch confirmed upstream of every one of the three blob call sites;
TS-71 retirement and TS-73 accuracy checked). Residuals owed to a small
coordinator pass, none reopening: R1 closure hoisting defeats the
per-tick allocation guard (move the IsDirty check into the ctor
lambdas); R2 concurrent MarkDirty during a flush is silently erased by
the trailing clear (needs the dirty-generation token); R3 NOTE — a
truncated-trailer PlayerDescription (options words zero) arms the seed
latch, newly wire-reaching via the timer.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fable-authored plan from the four seam-verified research lanes. Eight
design decisions stated per the campaign directive (reactable at gates):
the retail panel is acdream's ONE in-client settings surface (the F11
SettingsPanel was never rendered post-V11 — lane D's verified correction),
retail's 21-vs-batched wire split ships exactly with all three flush
triggers, the 2015-only PK-deaths row ships wire+store with a register
row, Configure Keyboard is the campaign's only rebind screen (DAT
ActionMap data, keybinds.json persistence), dead support-URL buttons
short-circuit to their own byte-verified retail failure strings, Exit to
Character Selection adapts to Exit Game behind retail's confirm dialog +
mid-air refusal, lane B group C re-points to server truth per CH3
precedent, and bots declare options by NAME in the K1 strict schema.
Slices: OP1 Runtime map/dirty/blob (+ the headless local-write fix both
lanes found), OP2 the two missing widget primitives + UIOption mappings,
OP3 shell + Gameplay tab vertical, OP4 Character tab, OP5 Chat tab, OP6
Config tab, OP7 headless characterOptions, OP8 Configure Keyboard, OP9
closeout + connected test script. U1 closed during planning: the Defaults
button restores DAT DBPropertyCollection values
(UIOption::InqDefaultGameplayOptionProperty @0x004ef8d0,
GetDIDFromEnumStatic(0x16, 2)).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Collects the post-gate polish left uncommitted by the killed round-5 agent
(S1/S3 + review fixes N1/N3/N4) and completes the missing S2 half:
- S1: UiText multi-line transcript + colored-run label now submit EVERY
line/run's outline pass before ANY fill pass, matching retail's
UIElement_Text::DrawSelf @0x00467aa0 whole-block walk. DrawStringDatPass
is exposed for block-level batching; single lines keep DrawStringDat.
- S2 (completed this commit): authored outline 0x21/0x22 now reaches every
text-bearing widget — UiButton, UiDatElement, UiField, UiMeter, UiMenu,
UiCatalogSlot — seeded from the element's effective-default state exactly
like UiText (BuildButton lifts the label-bearing Text child's authored
value first, same chain as the label color). Per-STATE outline switching
(dialog/character/combat buttons author 0x21 in state 0x3 only) is NOT
ported — filed as register row AP-192 in this commit.
- S3: ChatWindowController reconciliation comment corrects the misread
indicator action ids 0x10000514-17 -> 0x10000114-17 and re-attributes the
id-coincidence to the pagination widget's m_prevButton/m_nextButton, not
gmFriendsUI; register + window-shell research doc corrected to match.
- N1: LayoutImporter's duplicate per-state any-state-first-wins 0x21 read is
deleted — ElementReader.ApplyCanonicalLegacyProjection's DirectState-then-
effective-default resolution is the single source (the duplicate would have
lit state-0x3-only outlines permanently once S2 widened consumption).
- N3: the outline pass tints with the outline color's OWN alpha, not the
fill's (retail tints m_curOutlineColor and m_curTextColor independently).
- N4: the outline-inflated glyph SOURCE rect is clamped to the atlas bounds
with matching dest shrink, porting CreateCharRectPair @0x00441480's edge
behavior — edge glyphs crop instead of sampling a neighbour's texels.
Full Release suite: 12,610 passed / 4 skipped / 0 failed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Round 5 (2026-08-10): 'Good, looks good now.' Carried tail recorded in
the plan header and CLAUDE.md.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Campaign CH round 4, user-gate items 1+2. Root cause: retail ships a
second (background) glyph atlas per font, dilated 2px on every side,
plus two border-pixel scalars (Font.NumHorizontalBorderPixels/
NumVerticalBorderPixels) that acdream's font reader never read — so
even the pre-existing outline parameter drew almost nothing once
enabled. Landed together (either half alone is a no-op or a
regression):
- UiDatFont carries BorderX/BorderY from the DAT font resource.
- UiRenderContext.DrawStringDat inflates the background blit's source
and destination rect by that margin and restructures into retail's
exact two-pass whole-string outline-then-fill model
(UIElement_Text::DrawSelf), plus the 8-neighbour +-1px fallback for
fonts with no background atlas. Corrects the stale "property 0xd"
comment to the real ids, 0x21 (Outline) / 0x22 (OutlineColor).
- LayoutDesc property 0x21/0x22 import (ElementInfo.Outline/
OutlineColor, LayoutImporter.ReadState, ElementReader.Merge/
ApplyCanonicalLegacyProjection, DatWidgetFactory.BuildText) so every
authored-outline element across the DAT set is correct at once.
- SpewBox: RetailFontId corrected from a round-3 heuristic
(0x40000025) to the actually-authored 0x40000001 (18px bold serif),
Outline=true set on the controller's UiText. Fill colour stays the
user-gate-round-1-pinned yellow — font atlases are alpha-only
(PFID_A8), so there is no baked shading that could explain the
screenshot's gold as anything other than the outline itself.
- Chat transcript: default fill now seeds from its authored
ARGB(255,204,204,204) instead of an unrelated color-table slot
(ChatTranscriptRenderer.BuildLines takes the transcript's own
DefaultColor as a parameter); the 34-entry LogTextType table is
untouched, and every existing CH1 conformance test stays green
unmodified.
Regenerated the committed chat_2100006f.json fixture from the real
installed DAT, confirming end to end (not by missing-field default)
that the transcript carries no outline.
Tests: font-reader border fields + inflation math pinned against the
real DAT font, two-pass draw ordering/tint/inflation via a new
TextRenderer.DebugSpriteSegmentVerts test seam, property 0x21/0x22
import at both the ElementReader.Merge and StateDesc-property layers,
SpewBox font/outline, and the chat default-shade seed with the color
table proven untouched.
Full Release suite: 12,610 passed / 4 skipped / 0 failed
(AcDream.slnx, complete solution).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Item 3 (#364): every honesty marker is now gone from user-visible /help
text. AllegianceOverview/HouseOverview's "[IMPLEMENTED]" tags and trailing
"Subcommands NOT marked..." sentences, and Day/Log/Render/Motd's appended
"NOT YET IMPLEMENTED in acdream" tails, are removed; the underlying retail
text is corrected/completed against the pseudo-C's own pristine
consolidated data dumps (Log and Motd had been silently truncated; Render
was entirely acdream-authored and is replaced with the real retail usage
string). The three PARTIAL /help group topics (channels/chatting/commands)
are now COMPLETE verbatim listings: HelpStupidChannelHack's three
"vtable slot" operands, previously believed undecodable, are the same
pooled/mislabeled-data artifact this campaign has hit before (AP-113's
precedent) — reading the function's own disassembly for the push imm32
preceding each constructor call resolves all three directly. messagetypes
is now a real ported construction (IsLegalChannel's 14-id whitelist +
LogTextTypeToString's name table + the exact join/wrap format) instead of
an acdream summary. Register row AP-184 retired.
Item 5: the main window's 1/2/3/4 indicator buttons now toggle their
floating chat window on click, per the user's retail memory overruling
the earlier decomp-only reading. UIElement_Button::HandleButtonClick has
its own generic click-driven action dispatch (property 0x12) reaching the
same DoVisibilityToggleAction the Alt+1..4 keybinds use; the button
fixture confirms this half is genuinely armed, but the floating-window
fixture authors no matching listener-registration property, so the
generic mechanism has no proven target in the data on hand. Per
CLAUDE.md, the user's retail memory is the axiom regardless:
ChatWindowController.BindIndicatorClicks wires each indicator's click
through the same ToggleFloatingChatWindow chokepoint the keybinds use,
as explicit user-directed retail behavior. SetIndicatorOpen stays the
sole writer of the Selected mirror so the visual stays consistent
through the click round trip.
Full reconciliation in docs/research/2026-08-09-chat-retail-window-shell.md
§1.4. Campaign plan gets the round-4 findings section; items 1+2
(text-style) are under parallel research, item 4 passed, item 6 deferred
to the settings track.
Suite: 12,579 passed / 4 skipped / 0 failed (Release, complete solution),
up from baseline 12,553/4/0 — net +26 tests, zero regressions.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
SHOULD-FIX 1: RetailClientCommandCatalog's ~45 catalog leaf verbs were
showing acdream-authored Summary text for /help <verb> instead of
retail's own Detail_HelpType(2) text. Byte-swept every Help* handler
against the PDB-paired acclient.exe (verified MATCH), confirmed each
Detail/Summary branch by reading the actual decompiled if/else shape
(address order and string length both proved unreliable alone), and
fixed a sweep_weenie_strings.py 800-char truncation bug that silently
dropped several longer Detail branches. Resolved every ambiguous
CmdHashData-registered verb (hor/hr/hom/hoa/alh/ah/friends_add/
friends_remove/squelch/unsquelch) by reading for Binary Ninja's
nullptr-4th-arg decompiler artifact instead of trusting it. Coverage:
42 of 47 distinct catalog Definitions verbatim-extracted, 4
confirmed-null (index/clist/on/off register with a genuinely null help
pointer — DoHelp falls to UnknownCommand for these, now reproduced),
1 honest UNVERIFIED (messagetypes builds its text from a runtime enum
table, not a static string). ChatCommandRouter now prefers retail
Detail text over the catalog summary; RetailCommandHelpTable's class
doc no longer overclaims its own scope.
SHOULD-FIX 2: extracted the a5a7eb4f-class OnInterfaceText wiring into
a testable CreateChatViewModel method and added
ComposedChatViewModelWiresOnInterfaceTextToSpewBox, which the prior
FakeFactory-based test suite could never exercise.
SHOULD-FIX 3: retires register row AP-113. DoLifestone/DoMarketplace
print their own 0x1A refusal text (byte-recovered, UTF-16LE) instead
of falling through to the generic 0x26 fallback; ChatCommandRouter's
comment corrected to state the fallback's real scope.
SHOULD-FIX 4: corrected the divergence register's stale AP section
header sentence about AP-190's opacity default (refuted by cc582899).
NITs: (a) HeadlessStaticStateAudit routes through the injected
HeadlessDiagnosticWriter instead of Console.WriteLine; (b) a bounded
300-pump liveness diagnostic on the IsQuiescent conductor gate (no
retry, no behavior change); (c) fixed the #365 hydration test's doc
comment contradiction against diagnosis §8; (d) the 0x26 fallback
dispatches on WeenieErrorMessages' own Type instead of hardcoding
ClientLocal.
Full Release suite: 12,553 passed / 4 skipped / 0 failed (baseline
03404b71: 12,542/4/0; net +11 tests, zero regressions).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ChatVM gains an OnInterfaceText hook + ShowInterfaceText(text), the
App-layer composition wires it to RuntimeCommunicationState.AddText,
and ChatCommandRouter routes every retail-0x1A command refusal through
it instead of the chat log's 0x00 sink. UI.Abstractions still never
references Runtime directly; unwired hosts (headless, tests) fall back
to the chat log tagged ClientLocal so no text is ever silently lost.
Reclassified per register row AP-183 (DoChannelList/On/Off, DoAllegiance,
DoHouseAvailableList — the last also corrected to retail's own bad-house-
type string instead of a synthesized "Usage:" line) and newly wired two
sites that previously showed nothing at all (DoStupidChannelHack's bare
legacy-channel-verb refusal, DoReply's message-but-no-last-teller
refusal). The generic bad-args fallback now resolves WeenieErrorMessages
0x026 ("That is not a valid command.", retail's HandleFailureEvent(0x26))
instead of synthesizing "Usage: {Usage}". DoSpeaker/DoEndurance/DoTitle
are untouched — already correct at 0x00.
Also closes#367 (DoHelp's "Unknown command" fallback and the degenerate-
prefix refusal now reach the SpewBox too) and retires register row
AP-186, whose own filing proposed exactly this seam shape.
Full Release suite: 12,542 passed / 4 skipped / 0 failed (baseline
12,466/4/0 at ff2784ea).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
BLOCKER: ChatSettings.DefaultOpacity shipped retail's base ChatInterface
value (0.5) as ONE shared global default applied to every
RetailWindowManager-registered window, not just the four floating chat
windows retail itself fades. That faded the whole out-of-box registered
UI (radar, vitals, toolbar, main chat, ...) to 50% opacity, including
several windows that can never take keyboard focus and so were stuck at
0.5 permanently. Fixed to gmMainChatUI's 1.0/1.0 override
(0x004CD0F0) instead — retail-identical opaque presentation for the 11
non-chat windows and the main chat window; only the four floating chat
windows now diverge from retail's 0.5-while-idle default, and the
Settings -> Chat transparency slider remains fully user-settable.
AP-190 reworded and gains two new decomp-verified clauses: (3) retail
eases opacity toward its target by 5% of the delta per tick
(ChatInterface::ListenToGlobalMessage @0x004F3840, armed from the focus
element-messages at @0x004F5275) where acdream snaps -- deferred, needs
a UI frame-tick hook the opacity controller doesn't have; (4) retail's
focus predicate is the chat ENTRY FIELD specifically
(ChatInterface::IsTextEntryFocused @0x004F30A0) where acdream uses
any-focusable-descendant. Both findings + the pre-existing UiMenu.cs
PushAlphaAbsolute(1f) popup bypass are folded into the window-shell
research doc's opacity section.
NITs: fixed the stale "text bypasses the alpha" comment in
UiElement.DrawSelfAndChildren (CH6c already routed DrawStringDat/
DrawString through the same ApplyAlpha chokepoint as sprites/rects);
added RetailWindowManager.WindowUnregistered + wired
RetailWindowOpacityController to detach and forget a window unregistered
while it held focus (previously only Dispose detached, leaking any
window unregistered mid-focus for the rest of the session); added
post-Dispose no-op guards to the three Set* opacity mutators; added a
DrawString (BitmapFont path) alpha regression test and a DrawStringDat
outline/background-pass alpha test (the existing tests only ever
exercised the foreground/fill pass).
Also fixes RuntimeSettingsControllerTests.SettingsViewModelSavePreserves
SectionAndTargetOrder's now-stale "target-chat-opacity:0.5:1" expectation
(caught by the full-suite run this fix requires) to match the new 1.0
default.
Campaign ledger CH6c row updated to APPROVE-WITH-FIXES.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Retail's ChatInterface::SetOpacity (0x004F3120) fades the WHOLE composited
window surface with one alpha; UiRenderContext.ApplyAlpha already gated
DrawSprite/DrawRect/DrawFill (since 1da697ec, pre-CH6) but DrawStringDat and
DrawString still passed applyAlpha:false, so text stayed sharp over a
translucent window. Both now route through the same chokepoint.
RetailWindowOpacityController (new) subscribes to a new
RetailWindowManager.WindowRegistered event and drives every registered
window's live Opacity from keyboard-focus state, applied to EVERY window
(chat, floaties, vitals, toolbar, ...) rather than retail's ChatInterface-only
scope — register row AP-190, retiring the stale AP-40 "fixed 0.75, no focus
transition" row in the same commit.
Verified retail's shipped opacity defaults from the decomp (constructor
literals, no cdb needed): the base ChatInterface ctor sets
DefaultOpacity=0.5/ActiveOpacity=1.0, kept unmodified by the four floating
windows; gmMainChatUI's own ctor overrides the main window to 1.0/1.0
(always fully opaque). acdream ships one shared global default (0.5/1.0)
rather than replicating the per-class override — also AP-190. The linking
invariant (raising default above active drags active UP; lowering active
below default drags default DOWN — never a clamp) is ported verbatim as
ChatOpacityLink in AcDream.UI.Abstractions, shared by the live controller
and the new Settings -> Chat tab's two linked opacity sliders.
Persistence: ChatSettings.DefaultOpacity/ActiveOpacity round-trip through
SettingsStore; Save pushes both through IRuntimeSettingsTargets.SetChatOpacity
into the live controller, no restart required.
Rider (CH6a/b re-review): strengthened the grip-media regression guard past
a bare SpriteFile != 0 check — ChatLayoutConformanceTests now drives each
live grip through a real UiRenderContext/TextRenderer (backed by the
in-memory RecordingGpuDevice test double) and asserts the draw call chain
actually queued sprite geometry, via a new TextRenderer.DebugSpriteSegments
test-only accessor.
Full Release suite 12,459 passed / 4 skipped / 0 failed (baseline
12,420/4/0). No subagents, no client launches (session hard constraints);
pending the next connected user gate for visual confirmation.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The '(this commit)' placeholder appeared 13 times across historical rows;
only the CH6a/b rework references belonged to 1aa77099. Each historical
row restored to its true SHA.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Applies docs/research/2026-08-10-ch6ab-review-findings.md in full:
- BLOCKER 1: UiResizeGrip now carries its ElementInfo/resolve pair and
draws its own authored DirectState media (a synthetic parameterless
grip still draws nothing, preserving existing resize-drag tests).
DatWidgetFactory.BuildResizeGrip threads resolve through. All seven
live grips on the main chat window now resolve a non-zero sprite,
restoring the visible borders/corners CH6a silently dropped.
- SHOULD-FIX 2: ChatWindowState gains BroadcastTargetWindow, a sentinel
distinct from every real window id (0-4), fixing the bug where the
main window's explicit-addressing branch coincided with the broadcast
check (both were literal 0). SetFilter's main-window no-op is dropped
— the main window's filter is now genuinely settable. ChatWindowController
.Bind takes a ChatWindowState (the same canonical instance the floating
windows already share) and GetTranscriptLines builds a real accept
predicate instead of accept:null. Verified safe: ClientLocal (0x1A)
never reaches ChatLog (AddText routes it to the SpewBox and returns),
so nothing observable regresses.
- SHOULD-FIX 3: UiButton.SuppressSelfToggle stops the four chat-window
indicator buttons (DAT property 0x0B=true, no retail click handler)
from flipping their own Selected mirror on a stray click.
- SHOULD-FIX 4: generated and committed chat_floaty_2100005b.json from
the real installed dats; added the permanent RetailLayoutFixtureGenerator
entry. All three flagged FloatingChatWindowController assumptions
(input field, title bar, close button) are confirmed correct against
real data — no controller code changes needed. New finding: unlike the
main window, ALL EIGHT floaty border/corner elements are live Type-9
grips (the floaty's own title bar is its move handle), so a floaty
window resizes from every edge and corner.
- SHOULD-FIX 5: register row AP-189 documents the shared-500-entry/
200-line-tail vs retail's per-window 10,000-line scrollback depth gap.
- NITs 1-5: documented the filter-persistence-only-on-/saveautoui
asymmetry and the reconnect-preserves-filters intent; corrected the
research doc's modifier-mask mislabel and the "ONLY function" false
superlative; moved WrapText off ChatWindowController onto
ChatTranscriptRenderer, closing the circular dependency.
Full Release suite: 12,420 passed / 4 skipped / 0 failed (baseline
12,392/4/0 at 22020ef2; net +28 tests, zero regressions).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>