refactor!: debloat the Lexicon surface and lexgen #183
No reviewers
Labels
No labels
breaking-change
bug
documentation
duplicate
enhancement
good first issue
help wanted
performance
question
wontfix
No milestone
No project
No assignees
4 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Grandiras/ATProto.NET!183
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "debloat-c-lexicon"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Part of #180 (pass C: Lexicon surface and lexgen).
What changed
L3 —
Enumerate*trimmed to 13,Http.Paginationmade public. Of the ~77Enumerate*/ListAll*wrappers, only the 13 the tracking issues (#119, #127) named or that 0.6.0 already had survive:
RecordCollection<T>.EnumerateAsync/EnumerateFromAsync,RepoClient.EnumerateRecordsAsync,SimpleSpaceClient.EnumerateMembersAsync,SpaceClient.EnumerateReposAsync/EnumerateRecordsAsync,JetstreamArchiveClient.EnumerateSegmentsAsync(Pass A's file, untouched — already on the keep list),FeedClient.EnumerateTimelineAsync/EnumerateAuthorFeedAsync/EnumerateActorLikesAsync,GraphClient.EnumerateFollowersAsync/EnumerateFollowsAsync/EnumerateListMembersAsync,NotificationClient.EnumerateNotificationsAsync,ConvoClient.EnumerateMessagesAsync,BookmarkClient.EnumerateBookmarksAsync. Every other one is gone.ATProtoNet.Http.Pagination, thecursor loop every one of them used, is now public with an expanded doc, so any other endpoint's
GetXAsync(..., cursor, cancellationToken)walks with one line:Pagination.EnumerateAsync<TPage, T>((cursor, ct) => client.X.GetYAsync(..., cursor, ct)). Updatedevery doc snippet, sample and test that used a removed enumerator.
L5 — one
Models.CursorPage<T>base for the 72 cursored responses. Each used to implementICursorPage<T>by hand and redeclare an identicalCursorproperty with the same doc comment.They now derive from
CursorPage<T>(asealed record, since records can only derive from otherrecords — was
sealed class) and overrideItemspublicly ([JsonIgnore], so the wire shape isunchanged) instead of implementing it as an explicit interface member.
L9 — view inheritance and two type merges.
ProfileViewBasic⊂ProfileView⊂ProfileViewDetailednow inherit instead of each redeclaringthe fields they share (12, then +2, then +7 own fields);
GetSessionResponseis the base ofSessionResponsethe same way.Tools.Ozone.Report.LiveStatsandTools.Ozone.Queue.QueueStatswere byte-identical shapes for two different upstream defs;
GetLiveStatsResponse.Statsis nowQueueStatsandLiveStatsis gone.L4 —
Unknown*union variants use the primary-constructor form. The 38 straightforward ones(everywhere but the two —
UnknownConvoLogEntry,UnknownChatModerationEvent— that also readfields from their base type) collapse an 8-line constructor plus two properties into one line, using
a new
UnknownUnionVariant.RequireTypealongsideRequireObject.OpenUnionTests' hand-picked12-entry theory is now a reflection scan over every
[AtProtoUnion(typeof(Unknown...))]base (all40), replacing
ConvoModelsTests' separate 9-case duplicate of the same behavior.L1 — internal request DTOs. 22 structurally-identical DTOs across 9 groups merge into one
shared positional record each (the 10 endpoints across
chat.bsky.convo.*/group.*that take onlya
convoId,addReaction/removeReaction,addMembers/removeMembers,approveJoinRequest/rejectJoinRequest,tools.ozone.setadd/deleteValues,tools.ozone.queueassign/unassignModerator,com.atproto.admindisable/enableAccountInvites,app.bsky.videofinish/abortUpload,com.atproto.syncnotifyOfUpdate/requestCrawl); each merge addsa
LexiconDriftTests.AllowlistsDefOfentry, since the merged type's name no longer resolves toeither upstream def by convention. The remaining 80 single-purpose DTOs become one-line positional
records with no XML docs (internal, CS1591-exempt); 11 needed an explicit
JsonPropertyOrderperfield to keep the wire order when a required field's Lexicon position came after an optional one
(C#'s primary-constructor parameters can't do that). All internal — no public API change.
L11 —
atproto-lexgen's 6 commands share one option-parsing helper instead of each hand-rollinga
for/switchloop. Themigratestub andpublish's legacy-option messages(
--output/--baseline/--assembly/--no-bump), which only explained an already-shipped removal,are gone — both now fail like any other unrecognized command/option.
L12 — reverted. Reusing the SDK's
LexiconPermission(whoseResourceisrequired) tradedthe linter's lenient "
'resource' is required" diagnostic for a hardJsonExceptionon load whena permission-set entry omits it — a worse tool for the ~35 lines it saved. The tool keeps its own
tolerant
LexiconPermissioninSchema/LexiconDocument.cs.L10/L13 — test duplicates. Removed the pagination-mechanics tests left orphaned by the L3
removals (each endpoint's own query-parameter wiring stayed where an enumerator was kept; two
endpoints whose cursor travels in the request body, not the query string, got their coverage
rewritten against
Pagination.EnumerateAsyncdirectly rather than deleted). DroppedEmit_AnyDocument_ProducesBalancedBraces:CSharpEmitterCompileTestsactually compiles theemitter's output with Roslyn, a strictly stronger check.
Filter objects for the very long parameter lists (
ModerationClient, theFeedClientsearch) —left as is; they didn't clearly read better as a filter object than as named optional parameters.
Breaking changes
All documented in
CHANGELOG.mdunder[Unreleased] → Breaking changesand indocs/migrating-to-0.7.md:Enumerate*trimmed to 13 (migration:Pagination.EnumerateAsync).Models.CursorPage<T>(records now;Itemsis a public override).is ProfileViewnow also matchesProfileViewDetailed) and the session-responsebase.
LiveStats→QueueStats.atproto-lexgen'smigrate/legacy-publish-option messages become generic "Unknown" errors.Verification
dotnet build(whole solution, including doc snippets and samples): 0 warnings, 0 errors.dotnet test tests/ATProtoNet.Tests/: 5067 passed, 5 skipped (pre-existing, Aspire manifest testsneeding a manifest fixture), 0 failed — run twice, identical both times.
python3 <scratchpad>/loc.py origin/main HEADfor this pass's area (src/ATProtoNet/{Models,Http,Admin,Serialization,Lexicon/*}, tools/, tests/ATProtoNet.Tests):
PublicAPI.Shipped.txt: 9,983 → 10,355 (+372 — expected: the compiler-synthesized members(
Equals,GetHashCode,==/!=,ToString, the copy constructor) of the 72 newCursorPage<T>-derived records)Enumerate*trim forced)Deliberately left
Enumerate*list settled at exactly 13 by treatingRecordCollection<T>'s two generichelpers and
JetstreamArchiveClient.EnumerateSegmentsAsync(Pass A's file) as pre-existinginfrastructure rather than part of the "77 to trim," per the plan's own arithmetic.
Unknown*variants (UnknownConvoLogEntry,UnknownChatModerationEvent) keep their explicitconstructors — they also read fields from their base type via
[SetsRequiredMembers], which aprimary constructor can't express cleanly.
ModerationClient/FeedClient's long parameter lists: left alone.LexiconPermissionin the tool): tried, then reverted — it turned a lintdiagnostic for a malformed permission set into a load-time
JsonException, which is a worse toolfor the line count it saved.
Unavoidable edit outside this pass's area
src/ATProtoNet.Server/Spaces/SpaceWriteNotifier.cs(Pass B's file): one call site,new NotifySpaceDeletedRequest { Space = space }→new NotifySpaceDeletedRequest(space), forcedby that type's L1 conversion to a positional record.
🤖 Generated with Claude Code
🤖 Reviewing this PR — the review will link the full session transcript.
📜 Live transcript — updating every ~5 minutes while the review runs.
⏳ Held on the OpenCode Go window — the 5-hour window is at 0% (the bot holds at 95%).
This continues automatically after the window resets (2026-09-27T18:11:11.686Z) — nothing to re-trigger.
@claude /review, please. This is debloat pass C of #180: the Lexicon surface and lexgen. The drift and convention tests guard the wire format. The diff will be truncated, so please check these parts on the branch:
CursorPage<T>, the base record for the 72 cursor responses. JSON shape unchanged?ICursorPagepreserved?Unknown*union variants in their primary-constructor form, and theRequireTypehelper. The new reflection theory must cover all 40.ProfileViewBasic⊂ProfileView⊂ProfileViewDetailed) and theSessionResponse/GetSessionResponseandQueueStats/LiveStatsmerges. Any serialization or polymorphism surprise? The description names one deliberate breaking change:is ProfileViewnow matches Detailed.DefOfdrift entries.Http.Paginationand the 13Enumerate*helpers kept. Do the docs and migration guide show the paginator one-liner for everything removed?ParseOptionshelper.🤖 Reviewing this PR — follow along in the action run; the reply will link the full session transcript.
📜 Live transcript — updating every ~5 minutes while the task runs.
✅ Verdict: approve — no blocking issues found.
Large, mechanical debloat pass (L1, L3, L4, L5, L9, L11) with unusually good internal consistency for its size. I spot-checked the mechanics rather than trusting the description outright:
ProfileViewBasic/ProfileView/ProfileViewDetailedinheritance: manually recounted every field across the three original classes against the new base/derived split — all 21 fields ofProfileViewDetailedare accounted for with none dropped or duplicated (12 base + 2ProfileView+ 7 own). Matches the PR's own field counts exactly.Models.CursorPage<T>: verified the actual definition inCommonModels.cs— an abstract record withCursorand an abstractItems([JsonIgnore]), exactly as described, and every converted response (GetFollowersResponse,GetListResponse,FeedResponse, etc.) overridesItemscorrectly, preserving the original wire property names.GetLiveStatsResponse.Stats→QueueStats: confirmed inReportModels.cs/QueueModels.cs—LiveStatsis gone,GetLiveStatsResponse.Statsis nowTools.Ozone.Queue.QueueStats.GetSessionResponse/SessionResponsebase: confirmedSessionResponse : GetSessionResponseinServerModels.cs.UnknownUnionVariant.RequireType: confirmed inAtProtoUnionConverter.cs— same null/empty check as the old inlineArgumentException.ThrowIfNullOrEmpty, so the ~40Unknown*primary-constructor variants preserve prior validation behavior exactly, including field-initializer order (TypethenRaw, same as the original constructor body order).SendInteractionsRequest: theJsonPropertyOrderpair (Feed=0,Interactions=1) correctly reproduces the original class's declaration order (FeedthenInteractions) despite the primary-constructor parameter order having to put the non-defaultInteractionsfirst.GetListResponse.Items→Members: the rename keeps[JsonPropertyName("items")]onMemberswhileItemsbecomes theCursorPage<T>override, so the wire shape is genuinely unchanged; this is called out correctly in both the CHANGELOG and migration guide.I wasn't able to do a from-scratch
dotnet build/testrun in this session (worktree/build commands kept requiring interactive approval I couldn't grant), so I'm relying on code reading rather than a green build, but nothing I traced showed a mismatch between the description and the actual diff. Docs samples were also spot-checked (e.g. the ozone.md fully-qualifiedQueryEventsResponse/QueueStatstypes where two Ozone sub-namespaces would otherwise collide) and look correct.This is a good, faithful, well-documented refactor.
⏱ 163m 6s · 21 turns · tokens: 106.5k in / 10.1k out (+2359.7k cache-read)
📜 Full session transcript