| D-M8-01-0 |
2026-10-14 |
The ticket was built on an integration base and ported onto main with only its own commit; the English docs of main win. The rows stand in a section per ticket like the other ported tickets. The brief relies on no core rule and no contract change; D-W-51, D-W-100, D-W-120 and D-W-124 are planning rulings in this file. |
The tickets are built in parallel on integration bases; main takes each ticket as one pull request (precedent D-M3-17-8 and M1-23/3). |
| D-M8-01-1 |
2026-10-14 |
The tool code lives under services/server/cmd/bothy/importtool/ (D-W-124 (1)); the client is cmd/bothy/importtool/discord. TestDiscordClientImportsNoServerModule holds that go list -deps of the package contains no package under internal/ except internal/platform/egress and internal/kernel/.... |
bothy import is a subcommand of the one binary (component cmd), which may use every package; the test keeps the tool on the public API (22 2, R-IM-01). |
| D-M8-01-2 |
2026-10-14 |
Rate-limit guard: the route key is the path template plus the guild or channel ID; the bucket is the X-RateLimit-Bucket Discord last named for the route, else the path template, again plus that ID. A missing or unparsable wait (retry_after, Retry-After, and also X-RateLimit-Reset-After with Remaining: 0) counts as import.discord.max_rate_wait; any wait above it ends the call before sleeping with ErrRateWaitTooLong, also the wait for an exhausted bucket. After a waited-out 429 the bucket's refill time is cleared, so the retry does not wait twice. The retries after the last allowed 429 end with the new sentinel ErrRateLimited (status 429). 5xx (ErrServer) and transport errors are never retried in the client. |
22 4.3 items 1 to 4: fail towards waiting, never towards hammering (RK-76); the mirror's backoff is M8-05, an import run ends with exit 1 (22 10.3). |
| D-M8-01-3 |
2026-10-14 |
TestClientHasOnlyGet type-checks the non-test files of the package from source against export data (the approach of internal/wire/counters_static_test.go): exactly one call of http.NewRequest or http.NewRequestWithContext, whose method argument is the object net/http.MethodGet; no http.Request literal and no use of the field Method of a request; no exported name, method or field that matches post, put, patch or delete (case-insensitive); no exported function with a parameter named like a method or verb. TestFakeSeesOnlyGet drives every exported method against the fake, which answers every other method 405; TestNoTestPackageInBinary lists the dependencies of cmd/bothy. |
22 16 "Discord client without a write method", ADR 0043 ruling 1; the literal and field checks close the other ways to send another verb with the one construction. |
| D-M8-01-4 |
2026-10-14 |
MessagesAfter, MessagesBefore and MessagesAround return messages in ascending snowflake order, sorted by the numeric value, whatever order Discord used. |
Discord returns newest first; the import appends strictly in Discord order (22 6.2 thread_seq), so no caller may see another order. |
| D-M8-01-5 |
2026-10-14 |
Additions to the names of the brief (D-W-100): Config.Egress carries the egress.* values of the tool (allowed hosts, timeout, response size limit), because only an operator rule with allow_private may open the loopback fake and a consumer policy never can; the host policy of the client names exactly the host and port of import.discord.api_base over https. The User-Agent URL is the constant https://github.com/OWNER/bothy (the placeholder of repo_url in mkdocs.yml), the version comes from Config.UserAgentVersion. Further sentinels beside the brief's: ErrRateLimited, ErrUnexpectedStatus (any other status, a redirect included), ErrDecode (a body that is not the expected JSON, without its content), ErrInvalidID, ErrTokenHostMismatch; CallError has a Hint (the missing MANAGE_THREADS). Attachment.URL and Embed.URL are of type SignedURL, whose print forms show scheme, host and path only and whose Raw returns the full link (M8-02). Deps.Sleep is a Sleeper function. The fixture holds archive pages explicitly; a request beyond the last page gets the last page again. Fixture and test IDs are invented 8-digit strings, because the white-label rule of tools/denylist/generic.rules flags 17 to 20 digits as real Discord snowflakes; the client accepts 1 to 20 digits that fit an unsigned 64-bit number. |
The brief allows following the code where names differ; each addition keeps a rule of the brief (no proxy, token only to the API base, no secret in output) provable. |
| D-M8-01-6 |
2026-10-14 |
TestTokenOnlyToAPIBase checks the unexported header setter directly with another host, plain http and another port (refused, no header), and a redirect of the fake to a second host name, which the client does not follow (ErrUnexpectedStatus, one request, all on the API host). |
Every method builds its URL from the API base, so only the setter itself and a redirect can aim a request elsewhere. |