Skip to content

Go: optional fields are pointers — absence-capability by type, no waivers - #560

Merged
jeremy merged 30 commits into
mainfrom
go-optional-pointers
Aug 1, 2026
Merged

Go: optional fields are pointers — absence-capability by type, no waivers#560
jeremy merged 30 commits into
mainfrom
go-optional-pointers

Conversation

@jeremy

@jeremy jeremy commented Aug 1, 2026

Copy link
Copy Markdown
Member

Summary

Closes #436 with the decided Go end-state: every optional field in the generated client can represent absence, classified by type — no zero-value sentinels, no waiver list. Fixes #537 as a verified side effect.

Breaking for external pkg/generated consumers (deliberate, pre-1.0): optional value types are now pointers. The hand-written pkg/basecamp surface keeps its ergonomic value types.

What changed

Generator policy (commit 1):

  • go/oapi-codegen.yaml drops the global prefer-skip-optional-pointer: true — optional strings/booleans/numerics/time.Time/types.Date/nested structs generate as pointers (a value type cannot represent absence; omitempty made explicit zeros unsendable on request shapes).
  • scripts/enhance-openapi-go-types.sh: ten hand-maintained per-field pointerization passes and waivers collapse into one generic pass — optional non-nullable arrays on response-shaped schemas keep native []T (nil already represents absence). Request-shaped arrays stay pointers so an explicit empty array is sendable (omitempty drops a len-0 slice — the Create* subscriptions nil-vs-[] distinction), and nullable arrays keep the pointer for present-null. The time.Time/types.Date waivers are removed on principle: IsZero() is a zero-value sentinel, not absence.
  • The ten required-and-nullable jsonAdd fields (MyNote id/timestamps, Draft parent/scheduled_posting_at, SearchResult content/description, SearchType key, Wormhole color/destination_url) drop their hand-spelled pointer x-go-types — those compensated for the old flag suppressing oapi-codegen's nullable star and stacked to **T once it was removed.

Generated end-state (re-inventoried from the fresh diff): 770 omitempty fields — 711 pointers, 58 native slices, 1 interface{} (ValidationError.Details, where oapi-codegen intentionally never emits *interface{}), zero value-typed leftovers.

Wrapper/test absorption (commit 2): ~670 sites across 41 wrapper files + 6 test files. Reads use deref/derefInt64; writes preserve existing guard structure (&opts.X inside guards, omitzero() for zero-means-unset, ptr() for always-meaningful values); nested generated structs are nil-guarded with pointer presence replacing the old Id != 0 || Name != "" heuristics.

Two live crash bugs found and fixed (Go auto-derefs pointer field access, so gm.Category.Id compiles and then nil-panics at runtime): messages.go Category and events.go Details both crashed on payloads omitting those objects. Caught by test fixtures + a 97-site static sweep of pointer-field accesses; every other site verified guarded. events.go gains a red-proofed regression test (panics without the guard, passes with it).

Guard (commit 3): make go-check-optional-pointers (in make check + CI) classifies every omitempty field in client.gen.go: pointer/slice/map/interface pass, value types fail. No waiver list — the classifier is the policy. Red-proofed by injecting a value-typed violation; ValidationError.Details is the nil-capable negative control. SPEC §10 replaces the waiver-mechanics knob table with the resolved policy; rubric-audit.json 1B.4 → pass with history preserved.

Downstream regen (commit 4), incl. the #537 fix: the Ruby generator's datetime coercion matches the exact x-go-type spelling time.Time, so the previously starred fields were passing through as raw strings. De-starring fixes Draft.scheduled_posting_at + MyNote.created_at/updated_at coercion (Ruby suite 949 runs, 0 failures). The openapi.json change is otherwise Go-only: TS/Ruby/Python/Swift/Kotlin regenerate with zero real drift.

Wire-behavior notes (reviewed intentional improvements)

A handful of previously unconditionally-sent zero-valued query params are now omitted — each was a blank-default or an active hazard:

  • search.exclude_chat=false (Rails presence-checks make an explicit "false" string truthy territory), todos.completed=false, everything.*.page=0 (server defaults to 1), timesheet.person_id=0 (a bogus person filter), empty sort=/direction= pairs under compound guards.

Read-side: position: 0 on the wire now maps to &0 instead of nil (positions are 1-based, so unobserved in practice); present-but-empty nested objects (e.g. project.clientside) now yield a non-nil empty wrapper instead of nil — pointer presence is the truth.

Verification

  • Full make check green (verified REAL_EXIT=0 from the log, not a mid-run banner) — all six SDKs, conformance suites, all drift gates, the new guard.
  • Go: full test suite + vet green; go-check-drift/go-check-wrapper-drift/go-check-generated-drift clean (wrapper drift: 88 pairs, 1155 generated fields verified); conformance runner module builds clean.
  • Red proofs: guard (injected violation), events nil-Details regression test (stripped guard → panic), Go replay skip-marker tests unaffected.

Review round (all findings were real)

Guard had two silent holes of its own:

  • It matched only tags starting with json:, so every generated query parameter — which tags form: first — was exempt from the policy this PR establishes. Now matches omitempty in any tag: 859 fields checked, up from 770.
  • File.foreach inherited a US-ASCII external encoding under a non-UTF-8 locale and raised on client.gen.go's multibyte doc comments, so the guard died on the file it guards. Reads UTF-8 explicitly; verified under LC_ALL=C.

Request-reachability was a name match, now a $ref closure. QuestionSchedule is reached through Create/UpdateQuestion without carrying the RequestContent suffix, so its days array was wrongly kept native — an explicit empty array was unsendable. One schema was affected; the closure makes the class impossible.

Four wrapper sites carried len(x) > 0 guards forward from the value-typed world, where nil and empty were indistinguishable. With pointers they are not, and the guard silently drops an explicit empty array: hill_charts tracked/untracked, schedules CreateEntry participant_ids (UpdateEntry already used != nil — the file disagreed with itself), and questionScheduleToMap days. All now key on nil.

QuestionSchedule.Hour/Minute are *int so absence survives; the deref-then-address pattern manufactured a non-nil zero that reads back as "explicitly midnight". intPtrFrom carries nil through. Regression tests for both presence contracts, red-proofed against the pre-fix code.

#537 fixed at the mechanism, not the symptom. The de-star alone would have left ruby/scripts/generate-types.rb's exact-string x-go-type match in place, so a future *time.Time would regress silently. timestamp_go_type? now normalizes a leading star; types.FlexibleTime is deliberately excluded and documented (it also accepts date-only values — including it would be a behavior change, not a spelling fix). Regression tests cover all three affected fields plus the null case, red-proofed by reconstructing the pre-fix world (exact-match generator + starred spec → raw String → test fails), and the forward claim verified directly (new generator + starred spec still emits parse_datetime).

Synced with current main (#541 field-keyed 422, #557 Ruby pagination) and regenerated; rubric 1B.5 truthed up.

Closes #436. Fixes #537.
🤖 Authored with agent assistance by @jeremy.

jeremy added 4 commits July 31, 2026 19:09
Remove the global prefer-skip-optional-pointer flag so every optional
value type (strings, booleans, numerics, time.Time, types.Date, nested
structs) generates as a pointer: a value type cannot represent absence,
and omitempty made explicit zero values unsendable on request shapes
(SPEC.md §10, #436).

The enhance script's ten hand-maintained pointerization passes and
waivers collapse into one generic pass: optional non-nullable arrays on
response-shaped schemas keep native []T (nil already represents
absence), while request-shaped arrays stay pointers so an explicit
empty array is sendable, and nullable arrays keep the pointer for
present-null. The time.Time/types.Date waivers are removed on
principle — IsZero() is a zero-value sentinel, not absence.

The ten required-and-nullable jsonAdd fields (MyNote id/timestamps,
Draft parent/scheduled_posting_at, SearchResult content/description,
SearchType key, Wormhole color/destination_url) drop their hand-spelled
pointer x-go-types: those compensated for the old global flag
suppressing oapi-codegen's nullable star, and stacked to **T once the
flag was removed.

Generated end-state: 770 omitempty fields — 711 pointers, 58 native
slices, 1 interface{} (ValidationError.Details), zero value-typed
leftovers. The openapi.json change is Go-only: TS/Ruby/Python/Swift/
Kotlin regenerate with no real drift (openapi-stripped.json reflects
the removed x-go keys).

Source-breaking for external pkg/generated consumers — deliberate,
pre-1.0.
The hand-written surface keeps its ergonomic value types; the seam to
the generated client moves to pointer semantics:

- Reads: deref()/derefInt64() collapse absent to the zero value where
  the SDK type is value-typed; nested generated structs are nil-guarded
  instead of fabricating empty objects, replacing the old Id != 0 ||
  Name != "" presence heuristics with pointer presence.
- Writes: assignments inside existing zero-guards take the address;
  unconditional assignments use omitzero() (zero → nil → omitted,
  matching the old omitempty wire); loop counters and always-meaningful
  values use ptr().
- Latent panics the compiler cannot flag (Go auto-derefs pointer field
  access, so gm.Category.Id compiles and then nil-panics) are guarded:
  messages.go Category and events.go Details were live crashes for
  payloads omitting those objects; events gains a red-proofed
  regression test.

Wire-behavior notes (all in the PR body): a handful of previously
unconditionally-sent zero-valued query params (exclude_chat=false,
person_id=0, completed=false, page=0, empty sort/direction) are now
omitted — each was a blank-default or a presence-truthiness hazard
server-side.
…nd state

make go-check-optional-pointers classifies every omitempty field in
client.gen.go by type: pointers, slices, maps, and interfaces pass
(each can represent absence as nil); value types fail. No waiver list —
the classifier is the policy. Red-proofed by injecting a value-typed
violation; ValidationError.Details (interface{}) is the nil-capable
negative control. Wired into make check.

SPEC §10 replaces the waiver-mechanics knob table with the resolved
policy, and rubric-audit 1B.4 flips to pass with the history preserved.
openapi-stripped.json picks up the bare spellings. Side effect that is
a real fix: the Ruby generator's datetime coercion matches the exact
x-go-type spelling "time.Time", so the previously starred fields —
Draft.scheduled_posting_at and MyNote.created_at/updated_at — were
passing through as raw strings (#537). With the star gone they now
coerce via parse_datetime like every other timestamp. Ruby suite green
(949 runs, 0 failures).
Copilot AI review requested due to automatic review settings August 1, 2026 02:23
@jeremy jeremy added go spec Changes to the Smithy spec or OpenAPI breaking Breaking change to public API labels Aug 1, 2026
@github-actions github-actions Bot added typescript Pull requests that update TypeScript code ruby Pull requests that update the Ruby SDK labels Aug 1, 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: 65ea0b91b4

ℹ️ 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 scripts/check-go-optional-pointers Outdated
The Ruby type generator matched x-go-type by exact string, so a
pointer-spelled `*time.Time` silently degraded to a raw String. The
de-star in this PR fixed the three current manifestations by accident;
this fixes the mechanism: timestamp_go_type? normalizes a leading `*`,
so either spelling coerces, with a comment recording why
types.FlexibleTime is deliberately excluded (it also accepts date-only
values — including it would be a behavior change, not a spelling fix).

Regression tests cover the three affected fields plus the null case,
red-proofed against the pre-fix world (exact-match generator + starred
spec → raw String → test fails).

Also corrects two comments the pointer policy falsified: the Wormhole
smithy docs advertised `x-go-type "*string"` as the mechanism (it was a
workaround for the old global flag; Go now types these via the policy),
and the enhance script claimed TimelineEventData keeps a FlexibleTime
value type — generated Go has *types.FlexibleTime.

@cubic-dev-ai cubic-dev-ai 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.

7 issues found across 60 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="go/pkg/basecamp/schedules.go">

<violation number="1" location="go/pkg/basecamp/schedules.go:366">
P2: Creating an entry with `ParticipantIDs: []int64{}` now omits `participant_ids`, so callers cannot send the explicit empty array supported by the generated request type. Preserve nil-vs-empty semantics by checking whether the slice is nil rather than its length.</violation>
</file>

<file name="go/pkg/basecamp/hill_charts.go">

<violation number="1" location="go/pkg/basecamp/hill_charts.go:91">
P1: Callers can no longer send `"tracked": []` to explicitly clear/set an empty tracked list: an empty non-nil slice is omitted by this length guard. Preserve the nil-versus-empty distinction by assigning the pointer whenever the slice is non-nil.</violation>

<violation number="2" location="go/pkg/basecamp/hill_charts.go:94">
P1: Callers can no longer send `"untracked": []`: this guard omits an empty but present slice. Check for nil instead so explicit empty-array requests remain representable.</violation>
</file>

<file name="go/pkg/basecamp/checkins.go">

<violation number="1" location="go/pkg/basecamp/checkins.go:1090">
P2: Responses omitting schedule `hour` or `minute` now expose non-nil pointers to `0`, losing the absence distinction promised by `QuestionSchedule`. Preserve nil unless the corresponding generated pointer is present; explicit zero must remain non-nil.</violation>
</file>

<file name="scripts/enhance-openapi-go-types.sh">

<violation number="1" location="scripts/enhance-openapi-go-types.sh:94">
P2: Generated check-in requests cannot send an explicit empty `schedule.days`: `QuestionSchedule.Days` is a native `[]int32` with `omitempty`, so empty and absent both omit the field. `QuestionSchedule` is request-reachable through CreateQuestion/UpdateQuestion; retain its pointer or derive the response-only exemption from request reachability instead of the schema-name suffix.</violation>
</file>

<file name="scripts/check-go-optional-pointers">

<violation number="1" location="scripts/check-go-optional-pointers:33">
P1: The new optional-pointer guard crashes on the very file it guards. `File.foreach` under a non-UTF-8 locale reads client.gen.go as US-ASCII, and the generated file carries multibyte bytes (e.g. the → arrow in schema descriptions that flow into godoc), so `line.match(...)` at line 33 raises `ArgumentError: invalid byte sequence in US-ASCII` and the script exits non-zero. This breaks both `make go-check-optional-pointers` and `make check`, i.e. the governance gate central to this PR fails at runtime instead of printing the summary. Fix by reading the file as UTF-8, e.g. `File.foreach(path, mode: ‘r:UTF-8’)` (or `line.force_encoding(‘UTF-8’)` before matching).</violation>

<violation number="2" location="scripts/check-go-optional-pointers:33">
P2: This guard currently only matches struct tags that begin with `json:`, so optional query params with `form:` first are skipped. That leaves query fields outside the optional-pointer policy check and can let value-typed regressions pass CI. Matching the full tag and checking for `,omitempty` regardless of tag order would close that gap.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread go/pkg/basecamp/hill_charts.go Outdated
Comment thread go/pkg/basecamp/hill_charts.go Outdated
Comment thread scripts/check-go-optional-pointers Outdated
Comment thread go/pkg/basecamp/schedules.go Outdated
Comment thread go/pkg/basecamp/checkins.go Outdated
Comment thread scripts/enhance-openapi-go-types.sh Outdated
Comment thread scripts/check-go-optional-pointers Outdated
jeremy added 5 commits July 31, 2026 19:34
* origin/main:
  Carry pagination metadata on Ruby's lazy enumerators, cap with max_items (#557)
  Surface field-keyed 422 validation payloads in all six transports (#541)
Merges the field-keyed 422 work and Ruby pagination metadata, which
overlapped SPEC.md, rubric-audit.json, helpers.go, and client.gen.go.
Regenerated artifacts carry only the Wormhole doc-comment correction
from the previous commit. Also truths-up rubric 1B.5, which still
described #537 as an open exact-spelling gap.
Guard (both real defects, both silent):
- It matched only tags starting with `json:`, so every generated query
  parameter — which tags `form:` first — was exempt from the very policy
  this PR establishes. Now matches omitempty in any tag: 859 fields
  checked, up from 770.
- File.foreach inherited a US-ASCII external encoding under a non-UTF-8
  locale and raised on client.gen.go's multibyte doc comments, so the
  guard died on the file it guards. Reads UTF-8 explicitly; verified
  under LC_ALL=C.

Request-reachability is now the transitive $ref closure from every
*RequestContent schema rather than a name match. QuestionSchedule is
reached through Create/UpdateQuestion without carrying the suffix, so
its `days` array was wrongly kept native — unsendable as an explicit
empty. One schema was affected; the closure makes the class impossible.

Wrapper sites carried `len(x) > 0` guards forward from the old
value-typed world, where nil and empty were indistinguishable. With
pointers they are not, and the guard silently drops an explicit empty
array: hill_charts tracked/untracked, schedules CreateEntry
participant_ids (UpdateEntry already used != nil — the file disagreed
with itself), and questionScheduleToMap days. All now key on nil.

QuestionSchedule.Hour/Minute are *int so absence survives; the
deref-then-address pattern manufactured a non-nil zero that reads back
as "explicitly midnight". intPtrFrom carries nil through.

Regression tests for the presence contracts, red-proofed against the
pre-fix code.
Copilot AI review requested due to automatic review settings August 1, 2026 03:02

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.

@cubic-dev-ai cubic-dev-ai 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.

13 issues found across 65 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="go/pkg/basecamp/client_approvals.go">

<violation number="1" location="go/pkg/basecamp/client_approvals.go:243">
P2: A present zero `due_on` is still treated as absent. Presence is now represented by the pointer, so gate only on `ga.DueOn != nil` and preserve any supplied date.</violation>
</file>

<file name="go/pkg/basecamp/tools.go">

<violation number="1" location="go/pkg/basecamp/tools.go:289">
P1: Tool reads panic when the API omits `bucket`, because `gt.Bucket.Id` dereferences the newly optional `*RecordingBucket`. Guard `gt.Bucket != nil` before reading its fields.</violation>
</file>

<file name="go/pkg/basecamp/webhooks.go">

<violation number="1" location="go/pkg/basecamp/webhooks.go:293">
P2: Updating with `Types: []string{}` still omits `types` instead of sending an explicit empty array. Preserve the nil-versus-empty slice distinction when setting this pointer so callers can clear the list under the new optional-array semantics.</violation>

<violation number="2" location="go/pkg/basecamp/webhooks.go:494">
P2: Bio and Location are the only fields in webhookPersonFromGenerated that pass the generated pointer through directly (`p.Bio = gp.Bio`) instead of copying the value, so the returned WebhookEventPerson aliases the source generated.Person's storage. The rest of the mapping goes through deref(), and the sibling change in wormholes.go deliberately copies nullable-string pointers with the comment "Copy the value so the clean type doesn't alias gw" — so this is an inconsistency with the intended contract. If a caller dereferences and mutates p.Bio/p.Location, it will silently corrupt the underlying generated data. Consider copying the pointed-to value (e.g. `v := *gp.Bio; p.Bio = &v`) to match the rest of the mapping.</violation>
</file>

<file name="rubric-audit.json">

<violation number="1" location="rubric-audit.json:17">
P3: The rewritten 1B.4 note marks the criterion Met (pass=true, #436 closed) but preserves verbatim a historical block that still asserts 'REMAINING GAP (why pass stays false) ... tracked in #436' and instructs remediation via the now-removed 'global prefer-skip-optional-pointer: true default'. A reader running go-check-optional-pointers or auditing this entry will hit directly contradictory statements about the same pass flag. Trim the preserved note to the still-relevant Kotlin correction only (or explicitly mark the REMAINING GAP/#436 paragraphs as superseded) so the entry reads consistently with pass=true.</violation>
</file>

<file name="go/pkg/basecamp/schedules.go">

<violation number="1" location="go/pkg/basecamp/schedules.go:368">
P3: CreateEntry's new explicit-empty participant behavior has no regression coverage. Add a wire-level test for `ParticipantIDs: []int64{}` so a future simplification cannot silently omit the field again.</violation>
</file>

<file name="go/pkg/basecamp/checkins.go">

<violation number="1" location="go/pkg/basecamp/checkins.go:1085">
P2: A present schedule without `frequency` is returned as `nil`, losing fields such as explicit `days`, hour, or dates. Preserve the wrapper whenever `gq.Schedule` is non-nil; its zero-valued `Frequency` already represents the absent field ergonomically.</violation>

<violation number="2" location="go/pkg/basecamp/checkins.go:1104">
P2: Explicit zero interval values are lost on reads because these guards use zero as an absence sentinel. Check pointer presence and convert the pointed value, matching the new `Hour`/`Minute` handling.</violation>
</file>

<file name="go/pkg/basecamp/hill_charts.go">

<violation number="1" location="go/pkg/basecamp/hill_charts.go:121">
P2: An absent `updated_at` is exposed as a zero timestamp, so callers cannot distinguish it from a real value and re-marshaling may emit a sentinel timestamp. Preserve this optional timestamp as `*time.Time` in the wrapper and assign `ghc.UpdatedAt` directly.

(Based on your team's feedback about optional timestamps.) [FEEDBACK_USED]</violation>
</file>

<file name="scripts/enhance-openapi-go-types.sh">

<violation number="1" location="scripts/enhance-openapi-go-types.sh:95">
P3: Nullable-array nulls still collapse with absence: `*[]T` decodes both an omitted key and JSON `null` as nil, and cannot marshal an explicit null with `omitempty`. The stated present-null guarantee needs a tri-state nullable wrapper/custom marshal path; otherwise document the intentional collapse.</violation>

<violation number="2" location="scripts/enhance-openapi-go-types.sh:98">
P2: The generator pass that decides which optional arrays stay native []T keys off the request-body naming convention (`RequestContent$`) rather than the actual set of request bodies. Today `QuestionAnswerPayload` and `QuestionAnswerUpdatePayload` are request bodies that don't match that convention and aren't reachable from any `*RequestContent` schema, so they're excluded from `$request_reachable` — and would be treated as response-shaped if they ever carried an optional non-nullable array. The `go-check-optional-pointers` guard won't catch that (it passes any nil-capable `[]T`), so a future request payload could silently regress to an empty-array-unsendable state. Consider deriving the seed set from the actual requestBody `$ref`s in `.paths` rather than from schema names, which makes the closure robust to naming conventions and matches the stated 'request-shaped arrays stay pointers' intent.</violation>
</file>

<file name="scripts/check-go-optional-pointers">

<violation number="1" location="scripts/check-go-optional-pointers:32">
P2: The FIELD regex can silently skip optional fields, defeating the guard. The \A\t anchor requires exactly one leading tab and the type token \S+ cannot match a Go type containing whitespace (e.g. an inline anonymous struct or generic instantiation); such a field parses as no-match, the guard still counts other fields, and total.zero? never fires — so a value-type optional would evade the no-waiver invariant without any signal. Consider loosening the anchor/type capture so unparseable optional fields fail loudly rather than disappearing.</violation>
</file>

<file name="go/pkg/basecamp/cards.go">

<violation number="1" location="go/pkg/basecamp/cards.go:812">
P2: This change makes Position 0 indistinguishable from 'not provided'. For a column move, the position is an explicit destination; a caller that passes 0 (move to the front/highlighted slot) now has it omitted from the payload instead of sent as 0, which can yield different server placement or a missing-field rejection depending on the API. If 0 is a valid destination for a move, keep sending it explicitly (pointer to the value) rather than routing through omitzero, which drops zero by design.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread go/pkg/basecamp/tools.go
Comment thread go/pkg/basecamp/client_approvals.go Outdated
Comment thread go/pkg/basecamp/webhooks.go
Comment thread go/pkg/basecamp/checkins.go Outdated
Comment thread go/pkg/basecamp/checkins.go Outdated
Comment thread go/pkg/basecamp/cards.go Outdated
Comment thread scripts/enhance-openapi-go-types.sh Outdated
Comment thread rubric-audit.json Outdated
Comment thread go/pkg/basecamp/schedules.go
Comment thread scripts/enhance-openapi-go-types.sh Outdated
tools.go dereferenced the now-optional *RecordingBucket unguarded — a
live panic for any tool payload omitting `bucket`, and the third of its
kind. My earlier sweep missed it because it filtered on field NAMES that
are pointers in every struct, and Bucket is not. Redone as a
type-resolved sweep that binds each generated.T parameter and resolves
the field against that struct: tools.go was the only remaining hit.

Zero-as-absence sentinels replaced with pointer presence, which is the
whole point of the policy:
- checkins: a schedule present without `frequency` was dropped entirely,
  losing days/hour/dates with it; WeekInstance/WeekInterval/MonthInterval
  used `!= 0` guards so an explicit zero interval read back as absent.
- client_approvals: a present zero due_on was treated as absent.
- hill_charts: UpdatedAt was the only optional timestamp in the SDK still
  value-typed (its `omitempty` never fired — encoding/json does not omit
  a zero time.Time), so absence surfaced as 0001-01-01. Now *time.Time,
  matching cards/timeline/everything.

MoveCardColumn.Position now always transmits. BC3 documents this
endpoint's position as REQUIRED and zero-indexed, with `"position": 0`
as its own example, so omitzero silently broke a documented call — my
earlier acceptance of that flag was wrong.

webhooks: Types honors nil-vs-empty like the other list fields, and
Bio/Location copy the value instead of aliasing the generated struct's
memory into the returned wrapper.

Guard: an omitempty line the field pattern cannot parse is now a
FAILURE rather than a silent skip — a shape the generator starts
emitting must not quietly shrink coverage while the guard reports
success. Red-proofed with a nested anonymous struct.

Also: rubric 1B.4's preserved history no longer contradicts its own
verdict, the nullable-array comment states the real decode limitation
(encoding/json collapses null and absent) instead of claiming a
guarantee Go cannot make, and CreateEntry's explicit-empty participants
gain wire-level coverage. All new tests red-proofed.
Copilot AI review requested due to automatic review settings August 1, 2026 03:23

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: 0363b9e50a

ℹ️ 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/pkg/basecamp/hill_charts.go 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.

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

@cubic-dev-ai cubic-dev-ai 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.

4 issues found across 69 files

You’re at about 95% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="scripts/check-go-optional-pointers">

<violation number="1" location="scripts/check-go-optional-pointers:79">
P3: A future optional field using a named map/slice/interface will fail `make go-check-optional-pointers` even though it can represent absence as nil. Resolve named nil-capable declarations (including aliases), or classify by underlying type, before treating all other names as value types.</violation>
</file>

<file name="go/pkg/basecamp/optional_presence_test.go">

<violation number="1" location="go/pkg/basecamp/optional_presence_test.go:80">
P3: The httptest server + client wiring (DefaultConfig/BaseURL/ForAccount/StaticTokenProvider) is copied verbatim into five of the six tests. Extracting one helper that sets up the test server, decodes the request body, and returns a configured service would remove the duplicated boilerplate and make the pointer-presence assertions the only per-test content.</violation>
</file>

<file name="go/pkg/basecamp/timesheet_test.go">

<violation number="1" location="go/pkg/basecamp/timesheet_test.go:490">
P3: The new assertStrParam helper cleanly factors the string-param presence asserts, but PersonId repeats the same nil/expectation branching inline. Consider a parallel assertInt64PtrParam (or make the helper generic over *T) so all three optional params assert presence the same way and the PersonId branch stays as readable as the strings.</violation>
</file>

<file name="scripts/enhance-openapi-go-types.sh">

<violation number="1" location="scripts/enhance-openapi-go-types.sh:56">
P3: This binds the full schema map without using it, adding misleading dead jq state to the array-policy pass. Remove the binding.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread scripts/check-go-optional-pointers
Comment thread go/pkg/basecamp/optional_presence_test.go
Comment thread go/pkg/basecamp/timesheet_test.go
Comment thread scripts/enhance-openapi-go-types.sh

@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: 8b07b5c9a2

ℹ️ 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/pkg/basecamp/events.go Outdated
The transitive resolver landed in de5e6f1 with only scratch-artifact
proofs. That is the wrong place for the real protection: the enhancer's
own self-check computes the SAME closure it validates, so it cannot
catch a bug in that closure — a one-hop resolver misclassified every
schema behind an alias chain while the self-check passed throughout.

scripts/test-enhance-request-reachability drives the enhancer from
outside with synthetic specs whose correct answer is known
independently, covering direct, one-hop, two-hop, unused request-body
components, and response-only schemas. In make check.

Writing it immediately found a real defect: the empty-closure assertion
aborted on a spec whose only requestBodies component is UNUSED, or that
has no request bodies at all. It is now conditional on the spec actually
declaring request bodies, so a legitimate input no longer fails
generation while a broken walker on the real spec still errors
("82 operation(s) declare a requestBody but the closure is empty").

Also from review: the guard now recognizes the interface ALIAS form
(type X = interface{...}), not only declarations — missing it would
reject a field whose zero value is nil. And webhooks.go no longer calls
IsZero a "presence signal" on Recording's value-typed timestamps; it is
a legacy zero-value heuristic, presence is not recoverable there at all,
and a new test pins both halves so neither is simplified into the other.
Copilot AI review requested due to automatic review settings August 1, 2026 06:54

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.

@cubic-dev-ai cubic-dev-ai 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.

4 issues found across 70 files

You’re at about 99% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="scripts/check-go-optional-pointers">

<violation number="1" location="scripts/check-go-optional-pointers:82">
P2: Optional fields using named slice/map (or other nil-capable) types are rejected even though nil represents absence, so a valid future generated field will fail this required guard. Track named nil-capable declarations alongside named interfaces before treating a named type as a value.</violation>
</file>

<file name="go/pkg/basecamp/wrapper_propagation_test.go">

<violation number="1" location="go/pkg/basecamp/wrapper_propagation_test.go:63">
P3: Now that Person.CreatedAt/UpdatedAt are pointers, assertCreatorFullyPropagated dereferences them via gp.CreatedAt.Format(...) without a nil guard, so a fixture that omits either field would panic instead of failing the assertion. Every other pointer field in this helper is nil-safe (deref()/Company switch). Consider guarding or using deref for consistency.</violation>
</file>

<file name="go/pkg/basecamp/optional_presence_test.go">

<violation number="1" location="go/pkg/basecamp/optional_presence_test.go:259">
P3: These presence tests only assert empty vs non-empty for the propagated timestamps, so a source swap (e.g., CreatedAt populated from UpdatedAt) would still pass. Consider comparing to the exact expected RFC3339 string ("0001-01-01T00:00:00Z" for a present zero) alongside the presence check to catch wiring mistakes, per the team's timestamp-test guidance.</violation>
</file>

<file name="scripts/enhance-openapi-go-types.sh">

<violation number="1" location="scripts/enhance-openapi-go-types.sh:105">
P2: Generation now rejects an inline request-body schema even though no component schema belongs in this closure. `REQUEST_REACHABLE` is legitimately `[]` in that case, so remove or narrow this abort to avoid blocking enhancement when a future operation uses an inline body.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread scripts/check-go-optional-pointers
Comment thread scripts/enhance-openapi-go-types.sh Outdated
Comment thread go/pkg/basecamp/wrapper_propagation_test.go
Comment thread go/pkg/basecamp/optional_presence_test.go Outdated
…d state

events.go required a non-nil member inside details, so the present-empty
object the canonical fixture actually emits ("details": {}) mapped to
nil — callers could not distinguish "no membership changes recorded"
from "no details at all". Keyed on the pointer; red-proofed.

The guard now resolves NAMED nil-capable declarations generally —
interfaces, slices, and maps, in both declaration and alias form —
rather than interfaces alone. Verified precise, not permissive: named
slice/map/interface-alias all pass, a named struct is still rejected.

Removed a dead $all binding (and its orphaned comment describing a seed
that now lives in the shell preamble) from the array-policy pass.
Enhancer output verified unchanged.

Declining two P3 test refactors — extracting the httptest wiring into a
shared helper, and a generic assertInt64PtrParam. Both are legitimate
style points, but they are churn in tests that currently read
explicitly, on a PR that is already long; the duplication is four lines
of setup per case and each test states its own contract. Filing nothing:
if the test file grows further, the helper is the right move then.
Copilot AI review requested due to automatic review settings August 1, 2026 07:02

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.

An operation whose requestBody carries an INLINE schema references no
component, so the reachability closure is legitimately empty — and the
assertion I added last round aborted on it. Keying on "declares a
requestBody" was the wrong test; it now keys on whether any request body
actually references a component. Added to the durable test as case 6,
which fails against the previous assertion.

Test-quality follow-ups: assertCreatorFullyPropagated dereferenced
gp.CreatedAt/UpdatedAt without a nil guard, so a fixture omitting either
would panic instead of failing; it now treats nil as "must be empty".
And the Person timestamp test compared empty-vs-non-empty, which a
source swap passes — it now compares exact distinct values, red-proofed
by sourcing CreatedAt from UpdatedAt.
Copilot AI review requested due to automatic review settings August 1, 2026 07:12

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 1, 2026

Copy link
Copy Markdown
Member Author

Merging: 40 checks passing, zero unresolved threads, and this round produced no new findings.

Sixteen review rounds, which is a lot — worth recording what they actually bought, since almost none of it was the original change:

  • Three live nil-deref crashes the compiler could not flag, because Go auto-dereferences pointer field access: messages.go Category, events.go Details, tools.go Bucket. Found by a type-resolved sweep after my first name-based sweep missed the third.
  • A wire-behaviour bug fixed on upstream evidence: MoveCardColumn.position is documented required and zero-indexed, so omitzero broke the documented first-slot call. I initially waved this through and was wrong.
  • ~25 zero-as-absence sentinels removed across the wrapper layer — len(x) > 0 guards that silently dropped an explicit empty array, and !IsZero() guards that discarded a present zero the pointer was carrying.
  • Two guards that certified without checking: the optional-pointer guard skipped every query parameter (they tag form: first) and crashed outright under LC_ALL=C; the enhancer's reachability closure was one-hop and misclassified everything behind an alias chain.

Two things I got wrong and reversed:

  • I made HillChart.UpdatedAt a *time.Time on request, then reverted it when a later round showed x.UpdatedAt.IsZero() compiles and panics on nil — a silent break in caller code. Declined three subsequent re-asks for consistency; tracked with the full table and both options in Go wrapper surface types optional timestamps inconsistently (HillChart.UpdatedAt, SearchResult timestamps) #562.
  • I broke openapi.json and pushed it: an apostrophe in a jq comment closed the shell string, generation failed, and I missed it by piping to a grep instead of checking the exit status. Restored, verified semantically, and the script now carries an editing note.

Deferred deliberately, not forgotten: #562 (wrapper timestamp types — needs its own release-noted PR), #553 (replay decoder drift).

🤖 Authored with agent assistance by @jeremy.

@jeremy
jeremy merged commit 72c8b4b into main Aug 1, 2026
44 of 45 checks passed
@jeremy
jeremy deleted the go-optional-pointers branch August 1, 2026 07:17
jeremy added a commit that referenced this pull request Aug 1, 2026
#560 dropped `prefer-skip-optional-pointer`, so every optional query parameter
generates as a pointer: `ListProjectsParams.Page` is `*int32`, not `int32`.
The 36 wrapper call sites wired here assigned a value and stopped compiling:

    pkg/basecamp/boosts.go:88:55: cannot use page (variable of type int32)
      as *int32 value in struct literal

`pageParam` now returns `(*int32, error)` — nil for the out-of-range usage
error, `ptr(int32(page))` otherwise — and the 36 declarations become `var page
*int32`. The MaxInt32 guard and its single `#nosec G115` justification move with
it unchanged, and callers still only reach the helper once `Page` is positive,
so a successful call always yields a non-nil page.

The new policy makes absence expressible, so TestPageParamOmittedWhenUnset pins
it: a zero `Page` must not put `page` on the wire at all. The reaches-the-wire
tests alone would pass even if every request carried a stray `page=0`.

    go test ./pkg/basecamp -run TestPageParam                      REAL_EXIT=0
    GOOS=linux GOARCH=386 go test -c -o /dev/null ./pkg/basecamp   REAL_EXIT=0
    GOOS=linux GOARCH=arm go test -c -o /dev/null ./pkg/basecamp   REAL_EXIT=0

Red proof re-run against the rebased origin/main, test file only: 23/23
TestPageParamReachesWire subtests fail, plus both boundary tests (REAL_EXIT=1).
behavior-model.json stays byte-identical — #560 did not touch it either.
jeremy added a commit that referenced this pull request Aug 1, 2026
Widening the ListEntries params guard from "status is set" to "status is set OR
a page is selected" means the params struct is now built for a page-only call —
and `Status: &opts.Status` on an unset string is a non-nil pointer to "", which
the encoder sends as `status=` rather than omitting it. That displaces the
server's documented active-entries default, so asking for page 3 quietly
changed which entries came back.

Latent until #560: before optional query params became pointers, an unset
`Status` was a zero-value string that `omitempty` dropped.

Every other wrapper with this shape already used `omitzero`, which returns nil
for a zero value; `schedules.go` was the only site taking a raw address
(`rg ':\s+&opts\.' go/pkg/basecamp` finds one other, `reports.go`, whose guard
still fires only when the value is non-empty).

Red proof, before the fix:

    page_param_test.go:197: expected no status parameter when only Page is set,
      got "page=3&status="
    --- FAIL: TestPageParamDoesNotForceSiblingFilters
    REAL_EXIT=1
jeremy added a commit that referenced this pull request Aug 1, 2026
* Give wrapped-paginated operations their query params and pagination options

The Kotlin and Swift generators both treat "paginated" as "returns a bare
array", so an operation whose paginated array is nested under a response key
(x-basecamp-pagination.key) falls through the wrapped branch with half its
plumbing missing:

  - Kotlin TypeEmitter skips maxItems and toPaginationOptions() when building
    the operation's options class, while ServiceEmitter's wrapped branch handed
    `options` straight to requestPaginatedWrapped, which wants PaginationOptions.
  - Swift builds its queryItems array only for the plain and array-paginated
    shapes, but the wrapped call site passes `queryItems:` regardless.

Latent, not a regression. GetPersonProgress is the only wrapped-paginated
operation today and it has no optional query params, so its options parameter
already was PaginationOptions and Kotlin type-checked; Swift emitted no query
items because there were none to emit. The defect only surfaces once a wrapped
operation gains an optional query param, which generates a custom options class
and a query string that must reach the wire.

Regenerating the Kotlin and Swift trees against the current spec with these
fixes is a byte-identical no-op, so nothing generated changes here.

* Honor the page query param instead of documenting it as a no-op

Go's list wrappers have carried a Page field whose doc admitted it did nothing:
"The page number itself is not yet honored due to OpenAPI client limitations."
The limitation was ours — the Smithy spec simply never declared @httpquery("page")
on these operations, so no generated client could send it.

Declare it on the 38 list operations whose BC3 endpoints honor ?page= server-side
(every one resolves to a controller running geared pagination's
set_page_and_extract_portion_from, or, for Search, the SearchPagination concern
that reads params[:page] directly), matching the pattern the Everything
operations already used. Then wire the Go wrappers through: a positive Page now
reaches the generated params and the wire, and still disables auto-pagination as
it always did.

Three list endpoints are deliberately left alone because bc3 returns their full
collection unpaginated — webhooks#index, categories#index (message types), and
questions/answers/by_creator#index (answerers). Their Page docs now say the page
number is ignored rather than implying it will work someday, while noting that a
positive value still short-circuits auto-pagination, which is what the code does.

TestPageParamReachesWire covers one representative operation per wrapper file
against an httptest server, asserting ?page=3 reaches the wire.

* Reject page numbers that cannot survive the narrowing to int32

The generated params carry Page as an int32 while the wrapper options expose it
as a plain int, so every call site narrowed with a bare int32(opts.Page). On a
64-bit platform that silently wraps: a page above math.MaxInt32 arrives on the
wire as a negative page. gosec flags it as G115 across all 36 sites.

Route the conversion through a pageParam helper that rejects out-of-range values
with ErrUsage, mirroring the MaxInt32 guard RepositionTodolist already uses, and
carry the #nosec justification in one place instead of 36.

TestPageParamRejectsOutOfRange covers it: math.MaxInt32+1 yields a usage error
and never reaches the wire.

* Keep Kotlin source-compatible and document what page actually does

Two review findings, both real.

Codex: a paginated operation that gains its first optional query param moves
from `options: PaginationOptions?` to `options: <Operation>Options?`, which
breaks any caller already passing PaginationOptions and violates the pre-1.0
append-only guarantee in kotlin/README.md. My own PaginationTest needing a
rewrite was the tell. The generator now emits a compatibility overload beside
each affected method, taking PaginationOptions without a default so a bare
`list()` stays unambiguous. PaginationTest is reverted to the pre-PR call shape
and compiles unchanged, so it stands as a regression test for the guarantee.

cubic, on 22 threads: a positive `page` selects exactly that page in Go, but in
the five auto-paginating SDKs it is only the first page fetched — the walk then
follows Link rel="next" to the end of the collection. Verified by execution in
all six SDKs: asking for page 3 of a 4-page collection issued [3] in Go and
[3, 4] everywhere else. The divergence predates this PR, which widens it from 11
operations to 49; converging the five on Go's semantics changes documented,
shipped behavior and belongs in its own breaking change, tracked as #566.

What is not acceptable is shipping it silently, so: SPEC section 8 gains a
subsection with the per-SDK table and why max_items is not a page selector, and
all 49 page members carry a pointer to it. That pointer only reached Go,
TypeScript and Ruby, because the Kotlin and Swift generators emitted no
per-parameter docs at all — they now surface OpenAPI parameter descriptions as
KDoc and doc comments, collapsed to one line so spec wrapping cannot terminate
the comment early, and skipped for deprecated params whose description already
is the deprecation notice.

* Bridge PaginationOptions only where the options type actually changed

The compatibility overload was emitted for every paginated operation with an
optional query parameter — 55 of them — but only 22 needed it. An operation
that already had its own `<Operation>Options` class never had a
`PaginationOptions` signature to stay compatible with, and giving it the bridge
anyway leaves two applicable one-argument candidates, which is enough to make an
untyped callable reference stop compiling:

    e: PaginationTest.kt:624:43 Overload resolution ambiguity between candidates:
    e: PaginationTest.kt:625:44 Overload resolution ambiguity between candidates:

That is the blanket emission running against `BookmarksService::listMyBookmarks`
and `SearchService::search`, neither of which changed type here.

Emission is now keyed on PAGINATION_OPTIONS_COMPAT_OVERLOADS, the frozen roster
of the 22 operations that did move from `PaginationOptions` to their own options
class. Frozen in both directions: an entry cannot leave without breaking the
call sites the bridge exists for, and nothing new belongs in it.

The two directions are asserted at compile time in PaginationTest: an explicitly
typed reference to `CommentsService::list` fails if the bridge is dropped from an
operation that needs it, and an unconstrained `BookmarksService::listMyBookmarks`
fails if a bridge appears on one that does not.

For the 22 that keep it, an untyped callable reference now needs an expected type
to disambiguate — a narrower cost than breaking every caller that passes
`PaginationOptions(maxItems = ...)`, and called out in the generated KDoc.

* Pin the options-class constructor order so a new parameter cannot displace an old one

Generated options classes are data classes whose parameters all carry defaults,
so callers may construct them positionally. Constructor position is therefore
public API, and the pre-1.0 policy in kotlin/README.md is append-only.

The natural emission order violated that on its own: optional query params in
spec order, then the synthetic `maxItems` last. Because `maxItems` is last, every
new query parameter displaces it. Wiring `page` did exactly that to sixteen
shipped classes —

    ListCampfireLinesOptions(sort, direction, maxItems)
 -> ListCampfireLinesOptions(sort, direction, page, maxItems)

so `ListCampfireLinesOptions(null, null, 50)` silently stopped capping at 50
items and started requesting page 50.

The shipped order is now pinned per class in options-param-order.json, generated
into the tree beside the sources so the existing regenerate-and-diff drift gate
compares it like any other artifact, and read from the committed copy — which is
what the published API actually looks like — even when --output points at a
scratch directory. Parameters in the pin keep their positions, parameters absent
from it append in natural order, and pinned parameters the spec has since
dropped fall out. The sixteen classes are append-safe again:

    ListCampfireLinesOptions(sort, direction, maxItems, page)

`:generator:test` pins the rule itself, and runs in CI beside `:basecamp-sdk:check`.
Against a naive `orderOptionsParams` that returns the natural order, 4 of its 7
assertions fail.

* Keep the out-of-range page assertion compiling on 32-bit targets

The comment claimed the value was built at runtime, but `overflowing :=
math.MaxInt32 + 1` is a constant expression, so the whole package failed to
build wherever `int` is 32 bits:

    GOOS=linux GOARCH=386 go test -c -o /dev/null ./pkg/basecamp
    page_param_test.go:157:17: cannot use math.MaxInt32 + 1 (untyped int
      constant 2147483648) as int value in assignment (overflows)

Follow the guard todolists_test.go already uses for the identical MaxInt32
narrowing: skip under `strconv.IntSize < 64`, where an out-of-range page is
unrepresentable and the assertion has nothing to say, and build MaxInt32+1 by
incrementing so the source still compiles there.

The in-range half of the boundary is worth asserting everywhere, so
TestPageParamAcceptsMaxInt32 now pins that the largest page the generated int32
params can carry still reaches the wire. It needs no guard.

    GOOS=linux GOARCH=386 go test -c -o /dev/null ./pkg/basecamp   REAL_EXIT=0
    GOOS=linux GOARCH=arm go test -c -o /dev/null ./pkg/basecamp   REAL_EXIT=0
    go test ./pkg/basecamp -run TestPageParam                      REAL_EXIT=0

* Say what page does in each SDK's own README

"See SPEC section 8" only helps someone who has the spec repo. A developer who
installed the package has the README, and `page` behaves differently depending
on which package that is — so each README now says which one it is, in that
language, with a pointer to #566 for the planned convergence.

Go gains a Pagination section it never had: a positive `Page` fetches exactly
that page and disables auto-pagination, and the unpaginated endpoints are named.
The other five gain a subsection under their existing Pagination heading: `page`
is a starting offset that link-following continues from, `{ page: 3 }` on a
10-page collection returns pages 3-10 concatenated, pair it with `maxItems`.

Python needed this most — its generated services carry no docstrings at all, so
the README is the only place the keyword is described.

Also records the constructor-order pin under the Kotlin compatibility policy,
since that is the mechanism now backing the append-only promise.

* Keep the bridge nullable so a PaginationOptions? variable still resolves

The compatibility overload took a non-null `PaginationOptions`, which is
narrower than the signature it replaces. A caller holding a nullable variable —

    val saved: PaginationOptions? = ...
    comments.list(recordingId, saved)

matched neither the new options class nor the bridge, so it stopped compiling.
That is the same source break the bridge exists to prevent, just one call shape
over.

The bridge now keeps the old signature verbatim, `options: PaginationOptions? =
null`, and the operation's own options class arrives non-null beside it. Two
defaulted one-argument candidates would make a bare `list(id)` ambiguous, so
exactly one of the pair carries the default — and it is the pre-existing one,
which is what compatibility means here. A caller wanting "no options" already
has the default.

`PaginationTest.everyPreExistingCallShapeStillResolves` type-checks all six
shapes: no options, explicit null, non-null value, nullable variable, named
nullable, and the new options class. Red proof against the non-null bridge:

    e: PaginationTest.kt:624:116 None of the following candidates is applicable:
    e: PaginationTest.kt:654:30 Argument type mismatch: actual type is
       'PaginationOptions?', but 'PaginationOptions' was expected.
    e: PaginationTest.kt:655:40 Argument type mismatch: actual type is
       'PaginationOptions?', but 'PaginationOptions' was expected.
    > Task :basecamp-sdk:compileTestKotlinJvm FAILED

Generated bodies now reach `options` directly for these operations rather than
through a safe call, since the SDK builds with allWarningsAsErrors and a safe
call on a non-null receiver is a warning.

* Carry page as a pointer, the way optional query params generate now

#560 dropped `prefer-skip-optional-pointer`, so every optional query parameter
generates as a pointer: `ListProjectsParams.Page` is `*int32`, not `int32`.
The 36 wrapper call sites wired here assigned a value and stopped compiling:

    pkg/basecamp/boosts.go:88:55: cannot use page (variable of type int32)
      as *int32 value in struct literal

`pageParam` now returns `(*int32, error)` — nil for the out-of-range usage
error, `ptr(int32(page))` otherwise — and the 36 declarations become `var page
*int32`. The MaxInt32 guard and its single `#nosec G115` justification move with
it unchanged, and callers still only reach the helper once `Page` is positive,
so a successful call always yields a non-nil page.

The new policy makes absence expressible, so TestPageParamOmittedWhenUnset pins
it: a zero `Page` must not put `page` on the wire at all. The reaches-the-wire
tests alone would pass even if every request carried a stray `page=0`.

    go test ./pkg/basecamp -run TestPageParam                      REAL_EXIT=0
    GOOS=linux GOARCH=386 go test -c -o /dev/null ./pkg/basecamp   REAL_EXIT=0
    GOOS=linux GOARCH=arm go test -c -o /dev/null ./pkg/basecamp   REAL_EXIT=0

Red proof re-run against the rebased origin/main, test file only: 23/23
TestPageParamReachesWire subtests fail, plus both boundary tests (REAL_EXIT=1).
behavior-model.json stays byte-identical — #560 did not touch it either.

* Stop a page-only call from sending an empty status filter

Widening the ListEntries params guard from "status is set" to "status is set OR
a page is selected" means the params struct is now built for a page-only call —
and `Status: &opts.Status` on an unset string is a non-nil pointer to "", which
the encoder sends as `status=` rather than omitting it. That displaces the
server's documented active-entries default, so asking for page 3 quietly
changed which entries came back.

Latent until #560: before optional query params became pointers, an unset
`Status` was a zero-value string that `omitempty` dropped.

Every other wrapper with this shape already used `omitzero`, which returns nil
for a zero value; `schedules.go` was the only site taking a raw address
(`rg ':\s+&opts\.' go/pkg/basecamp` finds one other, `reports.go`, whose guard
still fires only when the value is non-empty).

Red proof, before the fix:

    page_param_test.go:197: expected no status parameter when only Page is set,
      got "page=3&status="
    --- FAIL: TestPageParamDoesNotForceSiblingFilters
    REAL_EXIT=1

* Drop the page-operation count rather than let it rot

SPEC.md said "49 operations accept a page query parameter"; the generated
openapi.json has 56 — 18 already declared it before this PR, and 38 more do now.
The number was wrong the moment the op list moved, and nothing in the repo
gates it: SPEC.md carried no other hardcoded operation count, so there is no
existing check to hook into and a corrected number would go stale the same way.

The claim the section actually needs is which operations accept `page`, not how
many, so it now states the rule. All 56 carry the SPEC pointer in their
generated docs (verified against openapi.json), and the concrete counts live in
the PR description, where a snapshot in time belongs.

* Scope the page contract to the operations it actually describes

Two claims added in this PR were too broad.

SPEC section 8 said every operation whose endpoint paginates server-side takes
a `page` parameter. Six do not: `ListWebhooks`, `ListMessageTypes`,
`ListChatbots`, `ListPingablePeople`, `ListQuestionAnswerers`, and
`ListUploadVersions` carry the pagination trait while their Basecamp index
actions return the whole collection, so there is no page to select. The section
now names them rather than inviting the trait to be read as the rule.

Worse, `GetMyNotifications` was given the `Semantics vary by SDK; see SPEC
section 8` pointer, and that operation has no pagination trait at all — the
generated services call `request`/`http_get`, never their pagination helpers. So
the pointer promised a caller that page 3 returns pages 3..N when the operation
returns exactly page 3, in all six SDKs. Its parameter now documents that
directly instead:

    Page number for paginating through read items. Defaults to 1. This
    operation is not auto-paginated in any SDK, so a page is returned as
    asked for and later pages are not followed.

Regenerated: 56 page parameters, 55 carrying the cross-SDK pointer and this one
carrying its own. behavior-model.json stays byte-identical.
jeremy added a commit that referenced this pull request Aug 4, 2026
v0.13.0 pointerized the optional fields across the Go surface (#560, #615,
#632) and shipped no way to build a pointer. `ptr[T any]` has been sitting
unexported in helpers.go the whole time, and go/README.md said nothing about
pointer fields, so every consumer hitting the migration writes their own
generic helper first.

This SDK's own test suite is the proof: schedules_test.go and
test_helpers_test.go hand-rolled strPtr, boolPtr, idsPtr and intPtr rather
than reach for the unexported one.

One generic Ptr rather than AWS-style typed constructors. The optional fields
span *string, *bool, *int, *int32, *int64, *time.Time and *[]int64 — a typed
set would need six names and still not cover ParticipantIDs *[]int64, where
a pointer to an empty slice is what removes every participant.

Deref covers the read direction, which is the more dangerous half. Go
auto-dereferences a value-receiver method call, so hc.UpdatedAt.IsZero()
still compiles against *time.Time and panics at run time on a chart that has
never moved. Deref is total: the zero value on nil.

The unexported ptr and deref stay as the internal vocabulary at hundreds of
conversion sites, but now forward to the exported pair, so the contract
callers get cannot drift from the one this package relies on.

Additive only: no existing exported signature changes.
jeremy added a commit that referenced this pull request Aug 4, 2026
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.
jeremy added a commit that referenced this pull request Aug 4, 2026
* MIGRATING.md: the v0.13.0 upgrade guide, silent breaks first

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.

* Fix the Go pagination advice, cut the raw-wire workaround, absorb #637/#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.

* Split silent breaks into no-signal and fails-at-runtime; absorb #629 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.

* Audit class A across all six SDKs; add Ruby's missing download retry

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.

* Correct four claims in the v0.13.0 guide that do not match the source

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.

* Re-derive every count against the final release commit

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.

* State the schedule-entry clear value per field instead of universally

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.

* Re-derive the per-SDK README banners against the final class A/B table

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.

* Correct the merged-PR count and the two claims the reviewers caught

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking Breaking change to public API go ruby Pull requests that update the Ruby SDK spec Changes to the Smithy spec or OpenAPI typescript Pull requests that update TypeScript code

Projects

None yet

2 participants