Blast-radius lens over Campaign FA slice FA1 (7be86f47,6bedbc47,5f9aa16f,4281750b,ee1124ca). The wire work is right -- both repaired builders re-derived against ACE's own handlers, all eleven allegiance version gates against AllegianceHierarchy::UnPack @0x005B7520, and both tree-assembly rules against Add @0x005B6E90. No handler-lane collision, no double registration, no cross-host source touched. The bookkeeping is not. MUST-FIX: register row AP-90 still cites the deleted AllegianceTree; lane D's seam map -- FA2's own contract -- is falsified in four places including a row that would make FA6 re-introduce the exact openness/panel-visibility bug FA1 fixed; plan decision D9 still says the 8 WeenieError strings were added when FA1 shipped the opposite finding; and the ledger's test totals state three mutually exclusive numbers (+46 implied, +50 stated, +49 measured from the diffs). SHOULD-FIX: the "UNWIRED" framing is untrue of ParseAllegianceInfoResponse (live behind @allegiance info -- vassal print order now reverses and a malformed tree now silences the command outright; both retail-faithful, neither pinned by an order-sensitive test); the id != 0 discard rule is missing; retail's monarch MayPassupExperience zeroing is missing; one new doc comment cites a nonexistent test class; and the confirmation triple now carries its discriminator two ways. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
21 KiB
Campaign FA slice FA1 — BLAST-RADIUS review
Date: 2026-08-12
Reviewer lens: blast radius — what else could these changes have touched,
along every axis the implementer may not have traversed (the C5b lesson:
enumerations leak along the axis not searched).
Target: 7be86f47, 6bedbc47, 5f9aa16f, 4281750b, ee1124ca on
claude/latest-commits-cb0c8f (diff base bb48d2c8).
Method: read-only. Repo-wide greps (src, tests, tools, docs,
.claude, in-repo memory/, persistent ~/.claude/.../memory/, AcDream.slnx),
retail decomp cross-check against docs/research/named-retail/acclient_2013_pseudo_c.txt,
ACE writer/handler cross-check, and targeted dotnet test --no-build -c Release
on the current (00:13 Aug 12, contemporaneous with the commits) binaries. No
build, no client launch, no subagents.
VERDICT: APPROVE-WITH-FIXES
The wire work is right. I independently re-derived the two repaired
builders against ACE's own handlers, all eleven allegiance version gates
against retail's AllegianceHierarchy::UnPack, and both tree-assembly rules
against AllegianceHierarchy::Add. Every one checks out, including the two
claims that most deserved scepticism (the sibling-order reversal and the
whole-message discard). Nothing in FA1 double-registers, shadows an existing
handler lane, or touches a cross-host path.
The bookkeeping is not right, and one of the gaps is the exact failure mode the C4 closeout handoff names as the most expensive in that campaign — a written contract that asserts a mechanism which no longer exists, left in place for the next slice to execute. FA1 falsified its own successor's seam map in four places and corrected none of them.
Four MUST-FIX (all documentation/bookkeeping, no code), five SHOULD-FIX.
MUST-FIX
MF-1 — The divergence register still cites the class FA1 deleted
docs/architecture/retail-divergence-register.md:298 (row AP-90):
"…acdream does not yet maintain live fellowship membership and its
AllegianceTreeis not wired into GameWindow."
4281750b deleted src/AcDream.Core/Allegiance/AllegianceTree.cs. CLAUDE.md's
register rule is binding in both directions — a commit that retires the cited
mechanism updates the row in the same commit. The deviation AP-90 records
is still real (radar relationship blips are modeled but undelivered), so this is
an edit, not a retirement: the row's evidence should now point at the flat
AllegianceMemberRecord list + AllegianceProfileLookups that replaced the
tree class, and at the fellowship parsers FA1 added.
This is also the row a future reader would hit first when asking "why doesn't the radar show fellows?" — it currently sends them to a file that does not exist.
MF-2 — FA2's and FA6's written contract was falsified and left in place
docs/research/2026-08-11-fa-acdream-seams.md (lane D) is not an inert dated
snapshot: the campaign plan's §1 table calls the lane docs "the spec's data
appendix", and 6bedbc47's own commit message points FA2 at this file by
section number. Four of its statements are now false, and one of them is
actively dangerous:
| Line | Says | Reality after FA1 |
|---|---|---|
:791 |
Allegiance state owner = RuntimeAllegianceState "wrapping existing AcDream.Core.Allegiance.AllegianceTree", precedent citation AllegianceTree.cs:56-162 |
Class deleted. FA2 is the very next slice and this is its seam-map row. |
:666 |
commands.Fellowship.SetOpen(gen, open) → BuildFellowshipUpdate |
BuildFellowshipUpdate no longer exists. Its renamed successor BuildFellowshipUpdateRequest is panel visibility, not openness. A FA6 implementer following this table literally re-introduces exactly the semantic bug FA1 just fixed. The correct target is the new BuildFellowshipChangeOpenness (0x0291). |
:662, :429 |
BuildFellowshipCreate(seq, name, openness, shareXp) at SocialActions.cs:123-138 |
Now 3 parameters (seq, fellowshipName, shareXp) at :144. The openness argument the bot-command row passes has no wire field to land in. |
:848 |
Open question 8: "BuildFellowshipCreate trailing-pad rule is pinned only by a self-consistent test, not against a retail/ACE reference. Lane A/B should confirm before FA sends it live." |
Answered by FA1 — and I re-confirmed it below (VC-3). The question should be closed with its answer, not left open for someone to re-do. |
Fix by amending lane D in place with dated FA1 correction notes (the campaign already treats these docs as living appendices), or by adding an explicit "FA1 contract corrections" subsection to the plan that FA2 must read first. Either is fine; leaving it is not.
MF-3 — Plan decision D9 still asserts the opposite of what shipped
docs/plans/2026-08-11-fellowship-allegiance-campaign.md:103-105:
"D9 — the 8 missing fellowship WeenieError strings are added in FA1 (two are on ACE's live send paths today; all resolve from the DAT string tables, never invented)."
and the FA1 slice-map row at :115 likewise lists "add the 8 WeenieError
strings" as slice scope.
5f9aa16f shipped the opposite finding and added no strings: retail's
Sept-2013 client has no display text for any of the 8, so acdream's current
silence is already faithful. I agree with the finding and with refusing to
invent English for them — but ee1124ca updated only the §9 ledger row, so the
plan's own decision list and slice map now contradict the shipped result. The
roadmap discipline is explicit that plan/reality divergence is fixed in the
session it is discovered.
Note the lane B research doc itself is not wrong here (it only ever claimed the ids were absent from acdream's table, which was true) — the overstatement is the plan's alone.
MF-4 — The ledger's test totals are internally inconsistent and do not reconcile
docs/plans/2026-08-11-fellowship-allegiance-campaign.md:181:
"full suite 13,149/4 skips/0 failures (baseline 13,103/4/0, net +50 tests)"
13,149 − 13,103 = 46, not 50. The row states two totals and a delta, and no two of the three agree.
My independent count from the actual diffs:
| File | Δ test cases |
|---|---|
AllegianceProfileVersionGateTests.cs (new) |
+20 |
AllegianceSmallEventsTests.cs (new) |
+7 |
ConfirmationTripleTests.cs (new) |
+4 |
FellowshipEventsTests.cs (new) |
+13 |
SocialActionsTests.cs (14 → 17 [Fact]) |
+3 |
AllegianceRequestsTests.cs (2 → 5 [Fact]) |
+3 |
WeenieErrorMessagesTests.cs (one new [Theory], 37 → 45 InlineData) |
+8 |
AllegianceTreeTests.cs (deleted, 9 methods, no theories) |
−9 |
| net | +49 |
So there are three mutually exclusive numbers (+46 implied, +50 stated, +49 measured) and at most one can be right. Most likely the baseline or the final figure was transcribed from a stale run. Re-run the full Release suite and correct the row — a ledger whose arithmetic does not close cannot serve as the automated gate it claims to be.
What I can confirm on the current Release binaries (built 00:13 Aug 12,
contemporaneous with the commits), --no-build:
AcDream.Core.Net.Tests— 877 passed / 0 failed / 0 skippedAcDream.Core.Tests— 4,676 passed / 0 failed / 1 skipped- FA1-family filter (
Allegiance|Fellowship|Confirmation|SocialActions) — 81 passed / 0 failed
Those two project totals are the anchors a corrected ledger row can be rebuilt from.
SHOULD-FIX
SF-1 — "UNWIRED / pure parse functions" is not true of ParseAllegianceInfoResponse
6bedbc47's commit message and the GameEvents.cs banner both frame FA1 as
"pure parse functions + typed records only, UNWIRED". That holds for everything
except the one function FA1 restructured that is live production today:
ClientCommandResponses.ParseAllegianceInfoResponse is registered at
src/AcDream.Core.Net/GameEventWiring.cs:192-198 and feeds
FormatAllegianceInfoLines → chat for the shipped @allegiance info command.
FA1 changed its observable output in two ways:
- Vassal print order flipped.
FindVassalsnow walks the record list in reverse (ClientCommandResponses.cs:257-259), andFormatAllegianceInfoLinesconsumes it directly at:605. A player with 3+ vassals sees them listed in the opposite order to yesterday's build. - Malformed-tree output went from partial to silent. An orphan /
self-parent / duplicate
treeParentnow returnsnullfor the whole message (:509-516), and the wiring'sif (info is null) return;prints nothing at all where the pre-FA1 parser printed a roster.
I verified both against retail rather than taking them on trust, and both are correct (see VC-4/VC-5) — so these are fidelity fixes to a live command, not regressions. But they should be described as such, and the reviewer-facing framing should not say "unwired". Two consequences:
- The change deserves a line in FA1's ledger row naming
@allegiance infoas the one live surface FA1 moved, so a user-visible ordering change is not a surprise at the FA5 gate. - The reversal is untested at the layer that shows it. The only
format-level test with vassals,
tests/AcDream.Core.Net.Tests/Messages/ClientCommandResponsesTests.cs:280-312(ParseAndFormatAllegianceInfoResponse_PatronAndVassals_RendersFullTree), has exactly ONE vassal, so it cannot distinguish forward from reverse order. Add a two-vassal case that pins the reversed order throughFormatAllegianceInfoLines, not just throughFindVassals.
SF-2 — Tree assembly omits retail's _id != 0 rejection
AllegianceHierarchy::Add @0x005B6E90 rejects any record whose own
_data._id is zero, on both the monarch branch and the child branch — and
a rejected Add discards the whole message
(UnPack @0x005B779D: if (Add(...) == 0) return 0;).
ReadAllegianceProfileBody implements the orphan, self-parent, and duplicate
rules but not this one (ClientCommandResponses.cs:494-524): a record with
CharacterId == 0 is accepted. ACE never emits a zero id in practice so this is
inert today, but the block's own comment enumerates retail's discard conditions
as if complete. One || record.CharacterId == 0 (plus the monarch check)
closes it.
SF-3 — Retail zeroes the monarch's MayPassupExperience; FA1's new column does not
Immediately after the record loop, UnPack does
(@0x005B77A7-0x005B77B3):
class AllegianceNode* m_pMonarch = this->m_pMonarch;
if (m_pMonarch != 0)
AllegianceData::SetMayPassupExperience(&m_pMonarch->_data, 0);
FA1's ReadAllegianceData computes MayPassupExperience per record
(ClientCommandResponses.cs:578-579) including the legacy
"HasPackedLevel absent ⇒ set it anyway" fallback, but nothing forces the
monarch's back to false. The field is unsurfaced today so nothing observes
it — but this is the one field whose comment explicitly argues fidelity ("a
port that also sets the bit is MORE faithful than one that does not"), so the
half of the retail rule that clears it belongs in the same commit. Cheapest
place is in ReadAllegianceProfileBody right after the monarch record is read.
SF-4 — New doc comment cites a test class that does not exist
src/AcDream.Core.Net/Messages/GameEvents.cs:572-574 — "see
ConfirmationResponseTests for the golden-vector / round-trip pair". The file
is tests/AcDream.Core.Net.Tests/Messages/ConfirmationTripleTests.cs
(class ConfirmationTripleTests). Trivial, but this is a citation in a file
whose whole value is the accuracy of its citations.
SF-5 — Name the confirmation-triple discriminator's owner before FA4
Ownership is currently split, which is the latent FA4 landmine the review brief
anticipated — though not in the shape it expected. There is no double
parser: 0x0274/0x0276 had typed parsers before FA1 and FA1 did not touch
them; 0x0275 is client-authored and never received, so FA1's new
ParseConfirmationResponse competes with nothing. The problem is
representational:
- FA1's new
ConfirmationResponse.Typeis the typedGameEvents.ConfirmationType(GameEvents.cs:577). - The two legs actually consumed in production —
CharacterConfirmationRequest.Type(:507) andCharacterConfirmationDone.Type(:530), read byGameplayConfirmationController.HandleRequest/HandleDone— remain bareuint.
So one triple now carries its discriminator two ways, and the typed one is on
the leg nobody reads. Before FA4 wires fellowship (type 4) and allegiance
(type 1) confirmations, either promote the two inbound records to
ConfirmationType or state in the enum's doc comment that it is deliberately
response-side only. Deciding now costs a sentence; deciding at FA4 costs a
migration across RetailDialogFactory/GameplayConfirmationController.
Verified clean
Stated plainly, so the next reader does not re-walk these axes.
VC-1 — The renamed/reshaped builders have no surviving callers or fixtures.
Repo-wide sweep for BuildFellowshipUpdate / BuildFellowshipCreate /
FellowshipUpdateOpcode across src, tests, tools, .claude, docs,
in-repo memory/, and the persistent memory dir. Production callers: none (H.2
left them caller-less and that still holds). tools/ and .claude/: no hits at
all. Persistent memory (~/.claude/projects/.../memory/): no hits.
memory/project_session_2026_04_18.md:137 is a dated session ledger
(historical record, correctly left alone). The wire-message catalog the brief
flagged does not exist — docs/research/2026-06-04-wire-message-catalog.md is
not in the tree (only the generator .claude/workflows/research-wire-message-catalog.js);
the MEMORY.md index entry pointing at it is a pre-existing broken link,
unrelated to FA1. docs/research/2026-07-29-wire-stack-audit.md:487,931 cites
both the deleted file and the old builder shape, but that doc declares its own
Base commit: b70b9832 in its header — a point-in-time snapshot, correctly
not FA1's to rewrite.
VC-2 — The deletion is complete. No references to AllegianceTree /
AllegianceNode / AllegianceMath remain in src/ (the one hit,
ClientCommandResponses.cs:247, cites lane C's DELETE verdict on the file and
is accurate as written). Nothing in docs/ISSUES.md. Nothing in AcDream.slnx
(project-level only; no per-file entries). Both directories
(src/AcDream.Core/Allegiance/, tests/AcDream.Core.Tests/Allegiance/) are
gone rather than left empty. The only docs/architecture hit is MF-1 above.
ChatChannelInfoTests.cs:28 is an unrelated test-name string coincidence, as
the commit message says.
VC-3 — The repaired BuildFellowshipCreate is byte-correct against ACE, and
the trailing-pad question is settled. PackString16L
(SocialActions.cs:389-403) already pads the string record to a 4-byte
multiple including its own u16 length, so 12 + name.Length is 4-aligned and
appending the u32 keeps it aligned — the pad the old builder added after its two
invented bools was never the string's pad. ACE reads exactly
ReadString16L() then ReadUInt32()
(GameActionFellowshipCreate.cs), and ACE.Common's ReadString16L skips the
same CalculatePadMultiple(2 + length, 4) pad. The golden vector at
SocialActionsTests.cs:65-83 ("Team", len 4, 2 pad bytes) is hand-derived and
correct. The other three builders match ACE too: 0x0290
ReadUInt32() newLeaderID, 0x0291 ReadUInt32() != 0, 0x00A6
Convert.ToBoolean(ReadInt32()), and GameActionType confirms
0x0290/0x0291/0x00A6 verbatim.
VC-4 — The eleven version gates match retail exactly. Walked
AllegianceHierarchy::UnPack @0x005B7520 line by line against
ReadAllegianceProfileBody. Confirmed: recordCount/oldVersion are the low/high
halves of one u32 (@0x005B754D, matching the ReadU16/ReadU16 pair); the
< 6 branch skips a single legacy spokesperson u32 only when >= 1
(@0x005B759B); titles at OfficersTitlesAdded; the four counters at
PoolsAdded; motd and motdSetBy together at MotdAdded; chatRoomID;
bind point at Bindstones; allegianceName and nameLastSetTime together at
AllegianceName; LockedState; ApprovedVassal. Against ACE's constant
oldVersion = 0x000B every gate is taken, so the cursor walk is identical to
the pre-FA1 unconditional read — the live @allegiance info field extraction is
unchanged. Also confirmed the first record carries no leading treeParent
(UnPack's if (ebp_1 != 0) guard at @0x005B776E).
VC-5 — Both tree-assembly rules are retail-verified, and ACE cannot trip the
discard. AllegianceHierarchy::Add @0x005B6E90 requires
Search(treeParent) non-null (orphan), parent->_data._id != _id
(self-parent), and Search(_id) == 0 (duplicate) — and
UnPack @0x005B779D returns 0 on any Add failure, i.e. discards the entire
message. The prepend is literal: eax_10->_peer = _vassal; esi->_vassal = eax_10; (@0x005B6F35-0x005B6F38), which is what makes the sibling walk
reverse. acdream's running knownIds set is equivalent to retail's
Search-from-monarch because both contain exactly the nodes added so far.
Separately I walked ACE's AllegianceHierarchyExtensions.Write for every
topology it can emit (monarch-only; patron-is-monarch; deep patron; player-is-
monarch): it always writes monarch → patron → self → vassals, so every
treeParent is already in the tree when its child arrives — the discard path
should never fire against ACE. Also checked the adjacent truncation risk and
cleared it: AllegianceNode.TotalVassals is Vassals.Count (direct only), so
recordCount always matches the records actually written.
VC-6 — No handler-lane collision, no double registration, no enum churn.
src/AcDream.Core.Net/GameEventWiring.cs is not in the FA1 diff — the
0x027C lane at :192-198 and the 0x0274/0x0276 lanes at :202-218 are
byte-identical to pre-FA1. GameEventType.cs is also untouched, and every
opcode FA1 parses (0x0003, 0x0020, 0x00A3, 0x00A4, 0x01C8, 0x01C9,
0x01CA, 0x027A, 0x02BE, 0x02BF, 0x02C0) was already declared there
with the right value — no additions, no duplicate values, no shadowing. The only
registration touching anything FA1 changed is the single 0x027C one, and
0x0020 has no registration at all (correct: FA2's job).
VC-7 — Cross-host symmetry holds. git diff --name-only bb48d2c8..ee1124ca
over src/AcDream.Headless, src/AcDream.App, src/AcDream.Runtime,
src/AcDream.UI.Abstractions returns empty. The headless host's behavior
cannot have moved: the only shared-path semantic change is
ParseAllegianceInfoResponse (SF-1), which the headless host does not consume —
its sole consumer is the chat-render registration in GameEventWiring.
VC-8 — The WeenieError commit is genuinely inert. Every changed line in
src/AcDream.Core/Chat/WeenieErrorMessages.cs is a comment; Resolve()'s
behavior is untouched (verified by filtering the diff to non-comment lines —
the result is empty). The new [Theory] proves all 8 ids resolve to no text,
matching the existing Format_0x051D_ReturnsNull_NoRetailCaseExists precedent.
VC-9 — No register row is owed by FA1 itself (beyond MF-1's correction). I audited the implementer's "everything is fidelity-fix or inert" claim against the two candidates the brief named:
- D5 (
shareLootdefensive parse). The plan's D5 promises "one register row records the knowing display divergence vs ACE's.3-at-nine-fellows server math". FA1 only parsesShareLootas a raw uint into an unsurfaced record field; no share percentage is displayed anywhere yet. The row is owed by the slice that renders the share column — FA4, per the plan's own wording ("display per D5" is FA4 scope at:118). Correctly not filed now. - D9 (the WeenieError outcome). No row: the register tracks acdream-vs-retail deviation, and FA1's finding is that acdream already matches retail (both silent). That two of the ids are on ACE's live send paths is an ACE-vs-retail difference, which the register does not track. Correctly not filed. (The plan text that this finding contradicts is MF-3.)
Coverage note
Every lens in the brief was traversed. Item 1 (builder-rename sweep) →
VC-1 + MF-2. Item 2 (deletion completeness) → VC-2 + MF-1. Item 3 (live
@allegiance info behavior) → VC-4/VC-5 + SF-1 (not bit-identical: two
retail-faithful but user-visible changes). Item 4 (GameEvents.cs additions vs
the 0x027C lane) → VC-6, clean. Item 5 (confirmation-triple ownership) →
VC-6 + SF-5 (no double parser; a representational split instead). Item 6
(test accounting) → MF-4. Item 7 (register/ISSUES/plan bookkeeping) → MF-1,
MF-3, VC-9. Item 8 (cross-host symmetry) → VC-7, clean.
No finding in this document is speculative: each cites a file:line, a retail address, or an ACE source file that I read.