Skip to content

MIGRATING.md: the v0.13.0 upgrade guide, silent breaks first - #642

Merged
jeremy merged 9 commits into
mainfrom
docs/v013-migration-guide
Aug 4, 2026
Merged

MIGRATING.md: the v0.13.0 upgrade guide, silent breaks first#642
jeremy merged 9 commits into
mainfrom
docs/v013-migration-guide

Conversation

@jeremy

@jeremy jeremy commented Aug 4, 2026

Copy link
Copy Markdown
Member

Adds MIGRATING.md at the repo root — the v0.13.0 upgrade guide — and links it from the root README, all six per-SDK READMEs, and every future GitHub Release body.

51 of v0.13.0's breaks survive a clean build. 47 give no signal at all — the call keeps working against a live server and does something different. 4 compile and then panic or raise, but only for a particular shape of response, so they pass your tests and fail in production. Label-generated release notes cannot express that, and until now it had no home in this repo.

Why one new file, and why the workflow change

The repo deliberately has no CHANGELOG. go/CHANGELOG.md and ruby/CHANGELOG.md were deleted in #115 ("Auto-generated release notes supersede these"), docs/ is gitignored as internal-only, and every release body since is machine-built: release-github.yml writes an Installation block and generate_release_notes: true appends the label-derived list. v0.10.0 through v0.12.0 carry not one hand-written line.

So there is no existing convention for narrative upgrade notes, and adding a per-version notes file resurrects exactly what #115 removed. Instead:

  • MIGRATING.md — what shipped, what you must do, what wrong behaviour you get if you don't.
  • release-github.yml now emits an ## Upgrading block linking MIGRATING.md at the tag, guarded on the file existing. I originally flagged this as a by-hand step; that was the wrong call — a hand-step with no gate is a step that gets skipped.
  • CONTRIBUTING.md records the rule.

Structure

Breaks your compiler will not catch leads the document, split into two classes that fail differently enough that merging them would mislead. Then one section per SDK, ordered by severity, plus an operator checklist, a "coverage: corrected and re-scoped" section for what did not ship, and known gaps.

SDK breaking no signal at all fails at runtime
Go 27 10 3
Swift 20 9 0
TypeScript 16 8 0
Python 14 7 0
Ruby 17 8 1
Kotlin 14 5 0

Counts are stated as-of 9de44b2a8 with derivations inline, and each in-flight change names exactly which numbers it invalidates, so the pre-tag pass is arithmetic rather than rewriting.

Review threads — all three fixed and resolved

P1, pagination. git show v0.12.0:go/pkg/basecamp/bookmarks.go returns before followPagination when page > 0, so Go was never a starting-offset SDK — "drop page to get the old walk" would have turned a bounded request into a full account-wide traversal. Item 1 is now scoped to the five SDKs where it holds (re-verified at the tag), with a #### Go is the exception subsection separating the services where the page number was already honoured from the fourteen carrying the v0.12.0 "not yet honored" doc.

P1, raw wire. The fmt.Sprintf + ac.Post example is gone, replaced by a There is no raw-wire migration path known-gap section citing AGENTS.md §4/§5. Swept the document; the one other ac.Post documents a real error-code change on an existing surface, so it stays, but its fabricated path is gone.

P2, misclassified panics. This was a contradiction with my own definition, not a wording problem — I wrote that a silent break "does not raise" and then listed panics under it. Applying the definition consistently moved four entries, not the three flagged: the three Go pointerization panics plus Ruby's Draft#scheduled_posting_at decode, which raises NoMethodError/TypeError and had the same defect. Two moved entries carry genuine no-signal residue, kept as sub-notes rather than double-counted. Body counts verified against the table by parsing the section, not by hand.

That check proves less than it sounds like, and the distinction matters. Parsing the section proves arithmetic consistency — the numbers agree with each other. It cannot prove inventory completeness, that every real break is present. Completeness came from a separate per-SDK audit against the definition, and that is what found Ruby's #563 missing from the guide entirely. No amount of parsing would have surfaced a break that was never written down.

Suppressed comment (Copilot), MIGRATING.md:1057 — correct and fixed. The Swift section said "Three new optional members are additive" but named one; the other two are required. Now singular, matching the TypeScript section.

Corrections carried into the guide

  1. TrashTodo was not a 404. bc3 draws resources :todos, only: %i[show edit update destroy]; DELETE /todos/:id returned 204 and destroy_status_param defaults to "archived" — every caller was archiving. The one Route corrections: nine flat routes bc3 only draws bucket-scoped, three operations removed #619 removal that takes away a working call. The nine re-pathed routes are a disjoint set whose 404 reassurance holds; the guide keeps them apart.
  2. Route corrections: nine flat routes bc3 only draws bucket-scoped, three operations removed #619 removed three operations, not nine. Nine were re-pathed.
  3. Hook operation identity differs by SDK. Go and Ruby emit a short verb; the other four emit the wire operation ID, where the todolist pair kept its names — so an allowlist holding UpdateTodolistOrGroup still passes the write and denies the new read.
  4. Kotlin's compatibility policy was wrong and is rewritten. It promised append-only source compatibility across 0.x minors; v0.13.0 violates that repeatedly and deliberately, and none of it is the withdrawn-endpoint carve-out. It now describes documented pre-1.0 breaking correctness releases. The binary-compat disclaimer is kept and sharpened.
  5. Publish Todolist.color and comments_app_url as required (#630) #637 adds no break to any SDK. color and comments_app_url did not exist on Todolist at v0.12.0 in any of the six — both arrived with One flat Todolist replaces Todolist, TodolistGroup and TodolistOrGroup #628 earlier in this same release — so from the guide's baseline nothing turned optional→required. I incremented all six counts for it and then reverted.

New in this push

  • Inventory re-derived: 238 → 247 (Model the bare field-map error bodies, then cloud_files and google_documents (#550, #551) #629 merged as a373b004c). Added/removed/moved lists are computed from openapi.json at both ends, not hand-edited: 14 IDs added, 5 removed, 11 same-ID route moves. Also flags that the Folders operations are drawn at /stacks, not /folders.
  • Cards: the due-date fix — a new section, and an operator-checklist entry for the half that is already true in production: every released SDK encodes "clear a card due date" as omission, and bc3 stopped treating omission as a clear, so that call is a silent no-op today. That is a reason to upgrade, not a hazard of upgrading. The SDK-side change (single PUT, "due_on": "", UpdateStepRequest.DueOn*string, hook collapse) is read from bf4371534 on fix/card-due-on-explicit-clear and marked as not-yet-merged.
  • The hook collapse is written as the inverse of the {Todolists,Update} split, because it fails the opposite way: allowlists do not start denying, but a denylist on {Cards,Get}/GetCard silently stops blocking the write it used to take down with it.
  • A defended defect class leaves the Cards surface: three named errorRaised kill cases go with the preservation GET (cards_write.json 8→5 cases, 3→0 errorRaised). Stated plainly, with the fact that the class stays pinned on Todos (2 errorRaised), so Cards is no longer the canary.

One infrastructure note

make check failed once on this branch with openapi.json is out of date while my spec files were byte-identical to main. Pristine origin/main passed the same gate, so it was stale gitignored spec/build/ state surviving a merge that changed the spec. rm -rf spec/build cleared it, no tracked file changed, and the suite is green. Worth knowing before someone "fixes" it by running make smithy-build and committing unrelated churn.

Verification

  • 150 headings, 29 internal anchors, all cross-file links resolve.
  • Class A/B body counts parsed and matched against the summary table — an arithmetic check, not a completeness one; completeness came from the per-SDK audit.
  • Class A/B body counts parsed from the document and matched against the summary table (31 + 4 = 35).
  • 238 → 247, the 300-field Go census, and the 41-gate count all reproduce from commands in the guide or from openapi.json/Makefile directly.
  • make check — 41 gates, exit code grepped from a captured log. (GetUpcomingSchedule renders reduced partials, so give it reduced shapes (#635, #641, #644) #648 takes it to 42.)

Copilot AI balanced review requested due to automatic review settings August 4, 2026 05:48
@jeremy jeremy added the documentation Improvements or additions to documentation label Aug 4, 2026
@github-actions github-actions Bot added typescript Pull requests that update TypeScript code ruby Pull requests that update the Ruby SDK go kotlin swift python Pull requests that update the Python SDK labels Aug 4, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7dd141feee

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread MIGRATING.md Outdated
Comment thread MIGRATING.md Outdated
Copilot AI review requested due to automatic review settings August 4, 2026 06:37
jeremy added a commit that referenced this pull request Aug 4, 2026
…#643

Addresses both P1 review threads on #642 and folds in the two PRs that landed
since the first draft.

Pagination (P1). Cross-SDK item 1 claimed `page` was a starting offset in every
SDK and told readers to drop it to restore the old walk. For Go that was
actively harmful: `git show v0.12.0:go/pkg/basecamp/bookmarks.go` returns before
followPagination whenever page > 0, so a positive Page already meant one
request, and dropping it converts a bounded call into a full account-wide
traversal. The item is now scoped to the five SDKs where it holds — re-checked
at the tag rather than assumed, since the universal claim had already failed
once — with a Go subsection splitting the two real cases: services where the
page number was already honored (Bookmarks, Drafts, Everything*, request
unchanged) and the fourteen carrying the "not yet honored" doc, which sent no
page at all and returned page 1's rows. Gauges is in neither; it had no page.

Raw wire (P1). The Forwards().CreateReply example built a path with fmt.Sprintf
and called the raw AccountClient.Post against a route with no upstream
coverage, which is what AGENTS.md "Never Do These" 4 and 5 forbid. Removed
rather than softened, and replaced with a known-gap section stating what a
hand-built path gives up. Swept the document: the one other hit documents a real
change to the raw client's error codes, so it stays, but its fabricated path is
gone and it now says it is not a suggestion to reach for the escape hatch.

#643 landed, so basecamp.Ptr and basecamp.Deref replace the hand-rolled ptr
helper throughout, the Go section opens with the 300-pointer census and a
command that reproduces it, and ParticipantIDs *[]int64 gets its own note: nil
leaves participants alone, a pointer to an empty slice removes every one.

#637 landed and does NOT add a break to any SDK. color and comments_app_url did
not exist on Todolist at v0.12.0 in any of the six — both arrived with #628
earlier in this same release — so from the guide's baseline nothing turned from
optional to required. Counts stay 27/20/16/14/16/14. Documented where it bites:
color is required-and-nullable so explicit null decodes, comments_app_url
rejects null and absence alike.

Also: kotlin/README's append-only source-compat promise contradicted this
release repeatedly, so it now describes documented pre-1.0 breaking correctness
releases; the binary-compat disclaimer is kept and sharpened. release-github.yml
links MIGRATING.md from every release body, guarded on the file, so the link
cannot be forgotten at tag time. "Silent" is defined as source/runtime-silent
against a live server, since a suite pinning request paths does catch some.

Counts are stated as-of 51d0d86 with derivations inline, and each in-flight
change names the numbers it invalidates so the pre-tag pass is arithmetic.
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Sensitive Change Detection (shadow mode)

This PR modifies control-plane files:

  • .github/workflows/release-github.yml

Shadow mode — this check is informational only. When activated, changes to these paths will require approval from a maintainer.

@github-actions github-actions Bot added the github-actions Pull requests that update GitHub Actions label Aug 4, 2026
@jeremy jeremy removed the github-actions Pull requests that update GitHub Actions label Aug 4, 2026
@jeremy

jeremy commented Aug 4, 2026

Copy link
Copy Markdown
Member Author

@codex review

Both P1s from the 7dd141feee pass are fixed in ce91ee784, and the changes were large enough to be worth re-checking:

  • Pagination — cross-SDK item 1 is now scoped to the five SDKs where the starting-offset claim actually holds, with a #### Go is the exception subsection. Go's two real cases are separated: services where the page number was already honoured vs. the fourteen carrying the v0.12.0 "not yet honored" doc, which sent no page at all. The harmful "drop page to restore the old walk" advice no longer applies to Go.
  • Raw wire — the fmt.Sprintf + ac.Post example is removed, replaced by a There is no raw-wire migration path known-gap section. The one other ac.Post in the document documents a real error-code change on an existing surface, so it stays, but its fabricated path is gone and it now states it is not a suggestion to use the escape hatch.

Also folded in since that commit: #637 and #643 (both merged), a rewrite of kotlin/README.md's source-compat promise, a release-github.yml change linking MIGRATING.md from every release body, and a precise definition of "silent".

Worth a specific look: I concluded #637 adds no break to any SDK, because color and comments_app_url did not exist on Todolist at v0.12.0 in any of the six — both arrived with #628 earlier in this same release — so the counts stay 27/20/16/14/16/14. If that reasoning is wrong the table is wrong.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ce91ee7848

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread go/README.md Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.

Suppressed comments (1)

MIGRATING.md:1057

  • This says "Three new optional members are additive" but names only one (groupPositionUrl). The next section (Todolist.color and .commentsAppUrl are new and required, #637) establishes that the other two new Todolist members are required, not optional — so there is exactly one new optional member here. The parallel TypeScript section states this correctly as singular ("Also new and optional: group_position_url"). Consider correcting the count to avoid contradicting the following section.
`URLProtocol` stub or cassette or decoding throws. Three new optional members are
additive: `groupPositionUrl`.

Copilot AI review requested due to automatic review settings August 4, 2026 07:27
jeremy added a commit that referenced this pull request Aug 4, 2026
…and cards

Addresses the remaining P2 and a suppressed Copilot comment on #642, re-derives
every count against main, and writes the cards due-date change.

The P2 was right, and it was a contradiction with this guide's own definition
rather than loose wording: "silent" was defined as "does not raise" and then
used to file nil-pointer panics. The section is now "Breaks your compiler will
not catch" — the property all of it actually shares — split into class A, no
signal at all, and class B, compiles then panics or raises but only when a
particular field is absent, so it passes every test where that field is
populated. Applying the definition consistently moved four entries, not the
three flagged: the three Go pointerization panics plus Ruby's
Draft#scheduled_posting_at decode, which raises NoMethodError and TypeError and
had the same defect. Two moved entries carry real no-signal residue, kept as
sub-notes rather than double-counted. Per SDK: Go 8A/3B, Swift 9A, TypeScript
5A, Python 4A, Ruby 2A/1B, Kotlin 3A — 31 + 4 = 35, unchanged in total. Body
counts verified against the table by parsing the section, not by eye.

The Swift section claimed three new optional Todolist members and named one;
the other two are required. Now singular, matching TypeScript.

Counts re-derived at 9de44b2: the inventory is 238 -> 247, not 241, since
#629 merged. Added, removed and route-moved lists are computed from openapi.json
at both ends rather than hand-edited — 14 IDs added, 5 removed, 11 same-ID moves
— and the Folders operations are flagged as drawn at /stacks, not /folders.

Cards get their own section. The half that matters most is true in production
today and is not caused by upgrading: every released SDK encodes "clear a card
due date" as omission, bc3 stopped treating omission as a clear, so that call is
a silent no-op right now. That is a reason to upgrade rather than a hazard of
it, so it sits in the operator checklist. The SDK-side change is read from
bf43715 and marked unmerged: single PUT, "due_on": "" as the clear encoding,
UpdateStepRequest.DueOn becomes *string, and the GetCard preservation read goes
away. The hook collapse is written as the inverse of the {Todolists,Update}
split because it fails the opposite way — allowlists do not start denying, but a
denylist on {Cards,Get} silently stops blocking the write it used to take down.
Removing the preservation GET also removes three named errorRaised kill cases
from cards_write.json; the class stays pinned on Todos, which still does a real
read-modify-write, so that is said rather than filed as a redundant-GET cleanup.
@github-actions github-actions Bot added the github-actions Pull requests that update GitHub Actions label Aug 4, 2026
@jeremy

jeremy commented Aug 4, 2026

Copy link
Copy Markdown
Member Author

@codex review

All three prior threads are fixed and resolved as of 9ac6f4475. The P2 was the interesting one: it was a contradiction with the guide's own definition, not loose wording.

The section is now Breaks your compiler will not catch, split into class A (no signal at all, 31) and class B (compiles, then panics or raises, but only when a particular field is absent — 4). Applying the definition consistently moved four entries, not the three flagged: the three Go pointerization panics plus Ruby's Draft#scheduled_posting_at decode, which raises NoMethodError/TypeError and had the same defect.

Specific things worth checking:

  1. The class A/B split itself. I kept no-signal residue from two moved entries as sub-notes rather than promoting them to separate class-A entries — e.g. SearchResult gaining omitempty silently changes what json.Marshal emits. That is a judgement call about counting; if you think it should be counted, the table changes.
  2. The cards hook collapse is the inverse of the {Todolists,Update} split, so I deliberately did not reason by analogy: allowlists do not start denying, but a denylist on {Cards,Get}/GetCard silently stops blocking the write it used to take down with it. Please sanity-check that direction.
  3. The inventory is re-derived, 238 → 247, with added/removed/moved computed from openapi.json at both ends rather than hand-edited. Model the bare field-map error bodies, then cloud_files and google_documents (#550, #551) #629 merged as a373b004c.
  4. Cards is documented as unmerged (bf4371534 on fix/card-due-on-explicit-clear), but the production half — omission no longer clears a card due date, so every released SDK's clear is a silent no-op today — is true regardless and sits in the operator checklist.

Also fixed the suppressed comment at MIGRATING.md:1057: the Swift section claimed three new optional Todolist members and named one.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9ac6f4475d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread MIGRATING.md Outdated
Comment thread MIGRATING.md Outdated
Comment thread MIGRATING.md Outdated
Comment thread MIGRATING.md Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 10 changed files in this pull request and generated no new comments.

Suppressed comments (1)

README.md:7

  • This root-level summary says all 4 runtime breaks fail "only on a payload that omits a particular field," but that only holds for the three Go breaks. Ruby's Class B break fails on the opposite condition — a record where the field is populated (the timestamp decodes to Time, so String methods raise); see MIGRATING.md:484-487 and the Ruby README, which correctly says "one that raises only on a record where the field is populated." The aggregate "omits a particular field" wording mischaracterizes that one case.
**Upgrading?** Read [MIGRATING.md](MIGRATING.md) *before* you bump the version. v0.13.0 breaks all six SDKs, and 35 of those breaks survive a clean build — 31 give no signal at all, and 4 compile and then fail at runtime only on a payload that omits a particular field.

jeremy added a commit that referenced this pull request Aug 4, 2026
Fourth review round on #642. Four findings, all upheld.

The allowlist framing was wrong in the direction that matters. I wrote that
fewer hook events are safe for an allowlist. True only if the allowlist named
both operations: one that names UpdateCard and deliberately omits GetCard used
to reject cards.update at its read, and after the collapse permits it end to
end. Both policy shapes now carry the warning, labelled, plus the observation
that they are the same hole seen twice — in each, the thing stopping the write
was the read, expressed once as an omission and once as an entry.

The class-A counting was inconsistent across all six SDKs, not the two flagged.
Python and Kotlin excluded changes their own prose called "no signal
whatsoever"; auditing every SDK against the definition moved the totals to 47
class A and 4 class B. The counting policy is now stated in the document so it
can be checked against a rule rather than an impression: one entry per distinct
change per SDK, counted where it bites; class A if any ordinary call-site shape
stays silent even when another is compile-caught; second faces annotated as
residue and counted once; raises-only-on-malformed-response is class B.

Two things fell out that were not counting problems. Ruby's #563 was missing
from the guide entirely — no mention of download_url anywhere in the chapter —
verified against source rather than prose: v0.12.0 http.get_no_retry, which
sent Accept: application/json and did not retry, became get_download calling
request_with_retry with retry_on: DOWNLOAD_RETRY_ON and accept: nil. Ruby now
has its own section. The same check confirmed Go's omission of #563 is correct,
because Go already retried at v0.12.0. Separately, the Go note claiming the
compiler catches only the pkg/generated half of Schedules().UpdateEntry was
false: UpdateScheduleEntryRequest's fields became pointers, so any pkg/basecamp
call site that set a field fails to build.

The class-B definition described only half its own membership. It said the
trigger is an absent field, but Ruby's entry fires only when the field is
populated. It now says both, and says plainly that class B is a property of a
call plus a response rather than of the call — the same method against the
other shape is not a break at all. Class A has no such dependency.

Stale counts in the chapter intros are fixed. The Go intro still said eleven
silent and two panics, which is the first thing a #go link shows, and Swift
claimed the most no-signal breaks, which stopped being true at Go ten.

Also folds in #652 (projected-example gate, stacked on #648, takes check-targets
to 43), moves #648 out of draft at cb438ce, and records that #647 is being
reworked Smithy-first because the generated UpdateCardStepRequestContent.DueOn
is *types.Date and cannot express "". The consumer-facing card shape is
unaffected by that rework. Re-derived against #648: 238 -> 247 with 14 added,
5 removed and 11 same-ID route moves survives unchanged.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 438ee2a70d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread MIGRATING.md Outdated
jeremy added 6 commits August 4, 2026 12:28
v0.13.0 breaks all six SDKs and 35 of those breaks are silent — no compile
error, no exception, no decoder failure. Label-generated release notes list
what merged; they cannot say what a consumer must react to or what wrong
behaviour they get if they ignore it. That had no home in this repo.

Adds MIGRATING.md at the root, linked from the root README and all six
per-SDK READMEs. Silent breaks lead the document, then one section per SDK
ordered by severity, plus an operator checklist, a "coverage: corrected and
re-scoped" section for what did not ship, and known gaps.

No CHANGELOG is reintroduced. The hand-maintained ones were deleted in #115
as superseded by auto-generated notes, and every release body since is
machine-built. CONTRIBUTING records the resulting rule: label-generated notes
say what merged, MIGRATING says what to do about it.

Corrections to the source drafts, each re-derived rather than repeated:

- TrashTodo was not a 404. bc3 draws `resources :todos, only: %i[show edit
  update destroy]`; DELETE /todos/:id returned 204 and set status to
  "archived", so every caller was archiving. It is the one #619 removal that
  takes away a working call, and it now carries its own carve-out.
- #619 removed three operations, not nine. Nine were re-pathed. Fusing the
  two sets is what made the blanket 404 reassurance look safe.
- Hook operation identity differs by SDK: Go and Ruby emit a short verb,
  the other four emit the wire operation ID, where the todolist pair kept
  its names — so an allowlist holding UpdateTodolistOrGroup passes the write
  and denies the new read.
- 238 -> 241 measured at the v0.12.0 tag and at c95d81c, not assumed.
- Kotlin binary compatibility is already disclaimed in kotlin/README.md;
  Swift has no written policy. Both are now stated rather than left unsaid.

recordings.get is documented as a known gap with a list-and-filter recipe
and its honest cost. The Go recipe compiles against this tree.

#637, #629 and #635/#641 were open at the time of writing and are recorded
under "Not in this release" rather than described as shipped.
…#643

Addresses both P1 review threads on #642 and folds in the two PRs that landed
since the first draft.

Pagination (P1). Cross-SDK item 1 claimed `page` was a starting offset in every
SDK and told readers to drop it to restore the old walk. For Go that was
actively harmful: `git show v0.12.0:go/pkg/basecamp/bookmarks.go` returns before
followPagination whenever page > 0, so a positive Page already meant one
request, and dropping it converts a bounded call into a full account-wide
traversal. The item is now scoped to the five SDKs where it holds — re-checked
at the tag rather than assumed, since the universal claim had already failed
once — with a Go subsection splitting the two real cases: services where the
page number was already honored (Bookmarks, Drafts, Everything*, request
unchanged) and the fourteen carrying the "not yet honored" doc, which sent no
page at all and returned page 1's rows. Gauges is in neither; it had no page.

Raw wire (P1). The Forwards().CreateReply example built a path with fmt.Sprintf
and called the raw AccountClient.Post against a route with no upstream
coverage, which is what AGENTS.md "Never Do These" 4 and 5 forbid. Removed
rather than softened, and replaced with a known-gap section stating what a
hand-built path gives up. Swept the document: the one other hit documents a real
change to the raw client's error codes, so it stays, but its fabricated path is
gone and it now says it is not a suggestion to reach for the escape hatch.

#643 landed, so basecamp.Ptr and basecamp.Deref replace the hand-rolled ptr
helper throughout, the Go section opens with the 300-pointer census and a
command that reproduces it, and ParticipantIDs *[]int64 gets its own note: nil
leaves participants alone, a pointer to an empty slice removes every one.

#637 landed and does NOT add a break to any SDK. color and comments_app_url did
not exist on Todolist at v0.12.0 in any of the six — both arrived with #628
earlier in this same release — so from the guide's baseline nothing turned from
optional to required. Counts stay 27/20/16/14/16/14. Documented where it bites:
color is required-and-nullable so explicit null decodes, comments_app_url
rejects null and absence alike.

Also: kotlin/README's append-only source-compat promise contradicted this
release repeatedly, so it now describes documented pre-1.0 breaking correctness
releases; the binary-compat disclaimer is kept and sharpened. release-github.yml
links MIGRATING.md from every release body, guarded on the file, so the link
cannot be forgotten at tag time. "Silent" is defined as source/runtime-silent
against a live server, since a suite pinning request paths does catch some.

Counts are stated as-of 51d0d86 with derivations inline, and each in-flight
change names the numbers it invalidates so the pre-tag pass is arithmetic.
…and cards

Addresses the remaining P2 and a suppressed Copilot comment on #642, re-derives
every count against main, and writes the cards due-date change.

The P2 was right, and it was a contradiction with this guide's own definition
rather than loose wording: "silent" was defined as "does not raise" and then
used to file nil-pointer panics. The section is now "Breaks your compiler will
not catch" — the property all of it actually shares — split into class A, no
signal at all, and class B, compiles then panics or raises but only when a
particular field is absent, so it passes every test where that field is
populated. Applying the definition consistently moved four entries, not the
three flagged: the three Go pointerization panics plus Ruby's
Draft#scheduled_posting_at decode, which raises NoMethodError and TypeError and
had the same defect. Two moved entries carry real no-signal residue, kept as
sub-notes rather than double-counted. Per SDK: Go 8A/3B, Swift 9A, TypeScript
5A, Python 4A, Ruby 2A/1B, Kotlin 3A — 31 + 4 = 35, unchanged in total. Body
counts verified against the table by parsing the section, not by eye.

The Swift section claimed three new optional Todolist members and named one;
the other two are required. Now singular, matching TypeScript.

Counts re-derived at 9de44b2: the inventory is 238 -> 247, not 241, since
#629 merged. Added, removed and route-moved lists are computed from openapi.json
at both ends rather than hand-edited — 14 IDs added, 5 removed, 11 same-ID moves
— and the Folders operations are flagged as drawn at /stacks, not /folders.

Cards get their own section. The half that matters most is true in production
today and is not caused by upgrading: every released SDK encodes "clear a card
due date" as omission, bc3 stopped treating omission as a clear, so that call is
a silent no-op right now. That is a reason to upgrade rather than a hazard of
it, so it sits in the operator checklist. The SDK-side change is read from
bf43715 and marked unmerged: single PUT, "due_on": "" as the clear encoding,
UpdateStepRequest.DueOn becomes *string, and the GetCard preservation read goes
away. The hook collapse is written as the inverse of the {Todolists,Update}
split because it fails the opposite way — allowlists do not start denying, but a
denylist on {Cards,Get} silently stops blocking the write it used to take down.
Removing the preservation GET also removes three named errorRaised kill cases
from cards_write.json; the class stays pinned on Todos, which still does a real
read-modify-write, so that is said rather than filed as a redundant-GET cleanup.
Fourth review round on #642. Four findings, all upheld.

The allowlist framing was wrong in the direction that matters. I wrote that
fewer hook events are safe for an allowlist. True only if the allowlist named
both operations: one that names UpdateCard and deliberately omits GetCard used
to reject cards.update at its read, and after the collapse permits it end to
end. Both policy shapes now carry the warning, labelled, plus the observation
that they are the same hole seen twice — in each, the thing stopping the write
was the read, expressed once as an omission and once as an entry.

The class-A counting was inconsistent across all six SDKs, not the two flagged.
Python and Kotlin excluded changes their own prose called "no signal
whatsoever"; auditing every SDK against the definition moved the totals to 47
class A and 4 class B. The counting policy is now stated in the document so it
can be checked against a rule rather than an impression: one entry per distinct
change per SDK, counted where it bites; class A if any ordinary call-site shape
stays silent even when another is compile-caught; second faces annotated as
residue and counted once; raises-only-on-malformed-response is class B.

Two things fell out that were not counting problems. Ruby's #563 was missing
from the guide entirely — no mention of download_url anywhere in the chapter —
verified against source rather than prose: v0.12.0 http.get_no_retry, which
sent Accept: application/json and did not retry, became get_download calling
request_with_retry with retry_on: DOWNLOAD_RETRY_ON and accept: nil. Ruby now
has its own section. The same check confirmed Go's omission of #563 is correct,
because Go already retried at v0.12.0. Separately, the Go note claiming the
compiler catches only the pkg/generated half of Schedules().UpdateEntry was
false: UpdateScheduleEntryRequest's fields became pointers, so any pkg/basecamp
call site that set a field fails to build.

The class-B definition described only half its own membership. It said the
trigger is an absent field, but Ruby's entry fires only when the field is
populated. It now says both, and says plainly that class B is a property of a
call plus a response rather than of the call — the same method against the
other shape is not a break at all. Class A has no such dependency.

Stale counts in the chapter intros are fixed. The Go intro still said eleven
silent and two panics, which is the first thing a #go link shows, and Swift
claimed the most no-signal breaks, which stopped being true at Go ten.

Also folds in #652 (projected-example gate, stacked on #648, takes check-targets
to 43), moves #648 out of draft at cb438ce, and records that #647 is being
reworked Smithy-first because the generated UpdateCardStepRequestContent.DueOn
is *types.Date and cannot express "". The consumer-facing card shape is
unaffected by that rework. Re-derived against #648: 238 -> 247 with 14 added,
5 removed and 11 same-ID route moves survives unchanged.
The opening warning said the runtime failures need a payload where a field is
absent. That holds for the three Go entries; Ruby's single class-B entry has the
opposite trigger. Draft#scheduled_posting_at and MyNote#created_at/#updated_at
run through parse_datetime, which returns nil for nil and a Time otherwise, so
.start_with? and Time.parse raise only when the field is populated. A reader
following the old text builds the wrong fixture and concludes they are
unaffected. Both directions are now named, here and in the root README.

Class A was described as breaking on every response. Most of it does, but two
groups do not: the error-message and validation entries need an error status to
reach the code at all, and the field-map half needs a body of a particular
shape; downloadURL's hop-1 retry changes nothing until a network error or one of
429/502/503/504 occurs. Stated as preconditions rather than as a blanket claim.

The Go pointer example said only the field selector panics. types.Date.String
has a value receiver, so Go rewrites t.DueOn.String() to (*t.DueOn).String() and
the nil dereference panics before String is entered. The same holds for IsZero,
Before, After and Weekday on Date and for Format, Sub, Unix and Year on
time.Time. The summary bullet already said both panic; the example contradicted
it.

The Accept-header note credited only Python. Ruby dropped it on the same hop:
get_download passes accept: nil, and request_headers sets the header only when
accept is truthy. Both are named, with the observation that the other four never
sent it on that hop at v0.12.0 either.

No counts are touched.
Rebased onto 2afc977 and re-measured rather than incremented. Eight PRs merged
since the branch was last updated, not the seven that carried the breaking
label: #647 was on the "Not in this release" list and had landed.

Counts. 55 class A and 6 class B, 61 surviving a clean build, up from 47/4/51.
Per SDK the class split is Go 12/4, Swift 10/0, TypeScript 9/0, Python 8/0,
Ruby 10/1, Kotlin 6/1, and the breaking-change column moves to 33/22/18/16/20/17.
The body parses back to those numbers rather than agreeing with them by hand.
The root README's aggregate sentence is re-derived to match, and now states both
halves numerically instead of "most" and "a few". The operation inventory is
unchanged at 238 -> 247 with the same 14 added, 5 removed and 11 same-ID route
moves, computed from openapi.json at both ends. check-targets is 43, and the
derivation is inline where the gate count was previously only projected. The
release spans 67 merged PRs, 15 labelled breaking; the gh commands that produce
both are embedded in the as-of block, with the note that a labelled PR is not
the same unit as an entry, which is why the per-SDK columns exceed 15.

#658 is class B, not class A. It does to five wrapper timestamps exactly what
#615 did to five others: QuestionReminder.RemindAt, ClientApprovalResponse's
CreatedAt and UpdatedAt, TimelineEvent.CreatedAt and WebhookDelivery.CreatedAt
compile untouched through a value-receiver call and panic on nil. #615's own
check could not see them because it keyed on the omitempty tag and these five
did not carry one. The audit is ten fields, and the entry names the near-miss
siblings that did not move, ClientApproval's pair in particular.

#664 splits. The public CreateScheduleEntryRequest fields were already string
and still are, so the wrapper half is silent: the RFC3339 ErrUsage guard is gone,
a bare date now creates an all-day entry, and a malformed value reaches bc3
instead of failing locally. That is class A. The generated
CreateScheduleEntryRequestContent went time.Time to string, which is a compile
error for pkg/generated importers. ReplaceScheduleEntryRequestContent is not a
migration from v0.12.0 at all; #632 introduced it. TypeScript and Ruby are
doc-comment only.

#647 is folded in as merged, with two corrections to what was written when it
was still a branch. It touches no schema, so the claim that it had to go
Smithy-first is withdrawn; UpdateCardStepRequestContent.DueOn was pointerized by
#560. And the v0.12.0 preservation GET was conditional, taken only when the
caller left due_on unaddressed, so the request-count table is scoped to that
path rather than presented as universal.

#648 adds no silent break anywhere. bc3's body is byte-identical before and
after, so nothing that was populated stops being so; the assignable's title was
never sent and is now spelled content. Every rename and retype is caught
statically in Go, Swift, TypeScript and Kotlin and raised immediately in Python
and Ruby, so it is one compile-or-runtime entry per SDK.

Two corrections nobody asked for. The Go class list opened "Go carries every
class-B break in the release", which stopped being true when Ruby's decode
entry moved into class B; it now claims only the panic-shaped ones. And
todos_write.json carries three errorRaised cases, not two, because #660 added a
bare-scalar kill.

#660 is a Kotlin class-B entry, which is new. Removing the client-wide isLenient
means a present, populated, wrong-typed scalar throws SerializationException
where it used to coerce to a string, and no signature moved to announce it. It
throws in the response decode, so on a write the mutation has already landed,
and it is not a BasecampException outside todolists.

#656 is Ruby class A, scoped tightly: only max_retries 0, only an ungoverned GET,
which means get_absolute and the Launchpad fetch rather than any operation
lacking a policy. Every other configuration is bit-identical.

Not in this release is now empty, and says so.
Copilot AI review requested due to automatic review settings August 4, 2026 19:52
@jeremy
jeremy force-pushed the docs/v013-migration-guide branch from 438ee2a to 317b5fc Compare August 4, 2026 19:52
@jeremy

jeremy commented Aug 4, 2026

Copy link
Copy Markdown
Member Author

Rebased onto 2afc97707 and re-derived every count

Rebase was clean — main's eight new commits touch none of this branch's files. The three merge commits the branch carried are flattened away; five doc commits replay unchanged, and git diff 438ee2a70 HEAD -- MIGRATING.md README.md is empty, so the rebase moved no content.

Eight PRs merged since the last update, not seven

#647 was on this guide's own Not in this release list and had landed (46b7f8225). It is labelled bug, not breaking, which is why it does not appear on the breaking roster — but the guide's counting unit is a documented consumer break, not a label, and the guide had already pre-committed to what to do if it landed. Absorbed accordingly.

Numbers, before → after

before after
class A (no signal at all) 47 55
class B (fails at runtime) 4 6
survive a clean build 51 61
Go — breaking / A / B 27 / 10 / 3 33 / 12 / 4
Swift 20 / 9 / 0 22 / 10 / 0
TypeScript 16 / 8 / 0 18 / 9 / 0
Python 14 / 7 / 0 16 / 8 / 0
Ruby 17 / 8 / 1 20 / 10 / 1
Kotlin 14 / 5 / 0 17 / 6 / 1
operation inventory 238 → 247 238 → 247 (unchanged)
check-targets 41 (projected 42, then 43) 43 (measured)
as-of commit 9de44b2a8 2afc97707

The class columns are not asserted — they are parsed back out of the document body and matched against the two summary tables and the root README. That check is arithmetic consistency, not inventory completeness; completeness came from reading each merged commit's diff against v0.12.0.

Commands

v12=$(git log -1 --format=%cI v0.12.0)

# merged PRs since v0.12.0 -> 67
gh pr list --repo basecamp/basecamp-sdk --state merged --limit 300 \
  --json number,mergedAt --jq "[.[]|select(.mergedAt > \"$v12\")]|length"

# breaking since v0.12.0 -> 15
gh pr list --repo basecamp/basecamp-sdk --state merged --label breaking --limit 300 \
  --json number,mergedAt --jq "[.[]|select(.mergedAt > \"$v12\")]|length"

# operations, both ends -> 238 and 247
jq '.operations|length' behavior-model.json
git show v0.12.0:behavior-model.json | jq '.operations|length'

# make check gate count -> 43
sed -n 's/^check-targets: *//p' Makefile | tr ' ' '\n' | grep -c .

# added / removed / same-ID route moves -> 14 / 5 / 11

The two gh commands are now embedded in the guide's as-of block, with the note that a labelled PR is not the same unit as an entry — one PR can break four SDKs — which is why the per-SDK columns exceed 15.

How the four new breaking PRs classified

Plus #647 (class A ×6, one Go compile-error entry) and #660 — a new Kotlin class B: removing the client-wide isLenient means a present, populated, wrong-typed scalar throws SerializationException where it used to coerce to a string, with no signature change to announce it. It throws in the response decode, so on a write the mutation has already landed, and it is not a BasecampException outside todolists.

Claims that were false and are now fixed

  1. "Go carries every class-B break in the release." Stopped being true when Ruby's decode entry moved into class B. Now claims only the panic-shaped ones.
  2. Cards: explicit due-date clears silently no-op against production — send "due_on": "" #647 "is being reworked Smithy-first." The merged commit touches no schema at all. UpdateCardStepRequestContent.DueOn was pointerized by Go: optional fields are pointers — absence-capability by type, no waivers #560, not Cards: explicit due-date clears silently no-op against production — send "due_on": "" #647, and the card-step wrapper never uses that struct — it hand-builds a map[string]any.
  3. The cards request-count table was presented as universal. All six SDKs took the preservation GET only when the caller left due_on unaddressed. A call naming dueOn was already a single PUT. Table is now scoped to that path.
  4. "todos_write.json keeps 2 errorRaised cases." It carries 3Kotlin: stop coercing a wrong-typed scalar into a String (#598) #660 added a bare-scalar kill.
  5. Not in this release listed three open items, all of which had merged. The section now states that nothing is in flight, and records the gate-count derivation that was previously only a projection.

Verified

  • make lint-actions check-readme-env-vars doc-constants-check — real exit code written into the log under a marker and grepped back: ZQMARKER_REAL_EXIT=0.
  • MIGRATING.md linked from the root README and all six per-SDK READMEs — 1/1/1/1/2/1/1, all non-zero.
  • Every internal anchor resolves; no duplicate headings introduced.
  • No reference anywhere to a file under docs/ — that directory is gitignored and internal-only, and this repo is public.

One thing I could not derive

The breaking changes column (33/22/18/16/20/17) has no derivation stated anywhere in the guide, and I could not reproduce the original 27/20/16/14/17/14 from the document's structure under any consistent rule — Go's 27 reproduces exactly as class A + class B + compile-error entries + pkg/generated bullets, but the same rule gives Swift 17 rather than 20. So I did not invent one. I applied the guide's own pre-committed increment protocol instead ("each entry names the specific derivations it invalidates, so the final pass is arithmetic rather than rewriting") and moved each SDK by the number of entries actually added: Go +6, Swift +2, TypeScript +2, Python +2, Ruby +3, Kotlin +3. Every other number in the table is derived and machine-checked. If that column should be reproducible too, it needs a stated rule, and that is a separate change.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 317b5fc4b7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread go/README.md Outdated
The Swift Behavioural bullet said an explicit "" clears any of the five
full-state fields. Only description does. "" on summary is accepted and
reads back "Untitled"; starts_at and ends_at are under
validates_presence_of in Schedule::Entry, so "" is rejected rather than
cleared; allDay is a boolean in every SDK, so "" does not typecheck at
all. The carve-out half grouped notify with the three clearable fields
even though it is a send directive with no state to clear.
Copilot AI review requested due to automatic review settings August 4, 2026 20:00
The six SDK README banners still carried the counts from before the Go
reclassification and the recount that followed it, summing to 51 where
MIGRATING.md and the root README say 61. Each banner now matches its row
in the class A/B table: Go 12+4, Swift 10, TypeScript 9, Python 8, Ruby
10+1, Kotlin 6+1. Kotlin also gains the runtime clause it was missing,
since its one class B entry throws on a present field carrying a JSON
number or boolean where the model declares a string.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7a782f805e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread MIGRATING.md Outdated
Comment thread .github/workflows/release-github.yml Outdated
The release spans 55 merged pull requests, not 67. The 67 came from comparing
GitHub's Z-formatted mergedAt against a git timestamp formatted with a local
offset, using jq's string >, which is lexicographic rather than temporal; it
wrongly swept in twelve PRs merged in the hours before the v0.12.0 tag instant.
The derivation embedded in the guide taught that same broken comparison, so it
now uses %ct and fromdateiso8601 and says why. The breaking count of fifteen is
unchanged, since all fifteen merged after the tag, so the class A/B split, the
per-SDK tables and the six README banners are untouched.

The header no longer calls 2afc977 the commit the release is cut from. That
commit is the last of the release content and the baseline the counts were
measured against, but it predates this guide; the tag is cut from main after
this merges, on a tree that contains the file the release body links to.

The release-body teaser claimed the guide covers only breaks with no exception
and no decoder failure. The guide documents six breaks that do fail at runtime,
including Ruby and Kotlin raises and a Kotlin decoder failure, so the teaser now
names both the silent class and the runtime one.
Copilot AI review requested due to automatic review settings August 4, 2026 20:20
@jeremy

jeremy commented Aug 4, 2026

Copy link
Copy Markdown
Member Author

Correction to a number I stated publicly: this release spans 55 merged PRs, not 67. Fixed in dd6564a3b.

I derived the 67 with a comparison that is not a comparison:

v12=$(git log -1 --format=%cI v0.12.0)   # 2026-07-31T17:57:46-07:00
gh pr list ... --jq "[.[]|select(.mergedAt > \"$v12\")]|length"

git log %cI emits an offset-formatted timestamp; GitHub's mergedAt is Z-formatted UTC. jq's > on two strings is lexicographic, not temporal, so "2026-07-31T20:11:00Z" > "2026-07-31T17:57:46-07:00" compares character by character and answers true for twelve PRs that merged before the tag instant of 2026-08-01T00:57:46Z — #534 #535 #529 #530 #536 #391 #540 #538 #539 #554 #542 #555.

The temporal form, now the one embedded in the guide:

epoch=$(git log -1 --format=%ct v0.12.0)   # 1785545866
gh pr list --repo basecamp/basecamp-sdk --state merged --limit 300 \
  --json number,mergedAt \
  --jq "[.[]|select((.mergedAt|fromdateiso8601) > $epoch)]|length"   # 55

Nothing else moves. The breaking count is 15 under both derivations — all fifteen merged after the tag, roster unchanged at #541 #549 #560 #574 #601 #615 #617 #619 #628 #632 #637 #648 #656 #658 #664. Every other figure in the guide is derived from that breaking roster, not from the total, so the 61 = 55 class A + 6 class B split, the per-SDK tables and the six README banners are all untouched and still reconcile.

The sentence carrying the bad number was, of course, the one warning that a hand-carried count is how these go wrong. The fix corrects the count and replaces the derivation it taught, with a comment explaining the trap so the document stops propagating it.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@jeremy

jeremy commented Aug 4, 2026

Copy link
Copy Markdown
Member Author

Merging last, as the release procedure requires — the tag is cut after this so the tagged tree contains the guide and the generated release-body link resolves.

Merging on independent review rather than a Codex pass at head: Codex has not fired at head on any PR this session, and Copilot's check has been failing repo-wide on its own infrastructure throughout.

Final audit at exact head dd6564a3b: 15 threads, all 15 fetched (no truncation), 0 unresolved. 31 reviews paginated and audited for Suppressed comments blocks — the correct marker; suppressed due to low confidence matches nothing and would have given a false all-clear. Two blocks found, both from earlier heads, both already fixed at the current head.

Numbers in this guide are now derived, not carried. The one I supplied was wrong and is worth recording: I reported 67 merged PRs since v0.12.0; it is 55. The derivation compared git log --format=%cI (offset-formatted, 2026-07-31T17:57:46-07:00) against GitHub's Z-formatted mergedAt using jq's >, which is a lexicographic comparison, not a temporal one. That wrongly swept in 12 PRs merged in the ~7 hours before the tag instant. Corrected to %ct + fromdateiso8601.

The guide had embedded that broken command, so it would have propagated the bug to whoever maintained the counts next release — that mattered more than the number, and the replacement carries a comment naming the trap.

The breaking count of 15 is unaffected, since all 15 breaking PRs merged after the tag instant. So the class A/B split (55 / 6), the per-SDK tables and the six README banners all stand — they derive from the breaking roster, which was correct throughout and is verified byte-identical.

Aggregates are machine-checked rather than eyeballed: table columns sum to 55 no-signal / 6 runtime / 61 total, MIGRATING prose and the root README both say 61 = 55 + 6, and the six per-SDK banners sum to 16+9+8+11+7+10 = 61 with the same split. All seven READMEs link the guide.

The release-body teaser deliberately carries no counts. That block is emitted for every release, so hard-coded numbers would be stale the moment v0.14.0 ships — the same hand-carried-number failure as above. It names the two classes; the guide carries the figures.

@jeremy
jeremy merged commit 931c36a into main Aug 4, 2026
48 of 49 checks passed
@jeremy
jeremy deleted the docs/v013-migration-guide branch August 4, 2026 20:39
jeremy added a commit that referenced this pull request Aug 5, 2026
#673 and #675 landed after the last measurement, so the guide's baseline
moves from 931c36a to 70d576b.

The counting method changes too, because the one committed here was
wrong. `git log v0.12.0..HEAD | wc -l` counts COMMITS, which equals PRs
only while every commit is a squash merge. The release-prep commit is
pushed straight to main and is not a PR, so from the moment the version
is bumped that count runs one high — it reported 59 where 58 PRs had
merged.

That is the third way this number has been wrong, each differently: a
lexicographic string compare on timestamps, then a temporal compare that
credited #556 to this release (its squash commit IS the commit v0.12.0
tags), and now a commit count meeting its first non-PR commit.

The rule that survives all three is the definition itself: reachable
from HEAD and not reachable from v0.12.0, applied to the merge commits
of PRs rather than to commits. Both derivations in the guide now use it,
and both were run to confirm they reproduce the numbers printed beside
them.

Committed directly rather than as a pull request, on purpose: a PR
carrying this number would increment the number it states. #642 shipped
wrong for exactly that reason — a document inside the release cannot
count itself.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation github-actions Pull requests that update GitHub Actions go kotlin python Pull requests that update the Python SDK ruby Pull requests that update the Ruby SDK swift typescript Pull requests that update TypeScript code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants