-
Notifications
You must be signed in to change notification settings - Fork 11
BC5 readiness: Go wrapper forward-compat audit + field-level drift check #309
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
26 commits
Select commit
Hold shift + click to select a range
b7212f0
wrappers: standardize nested Person on personFromGenerated
jeremy 3ab4dd8
wrappers: cross-cutting Recording-shaped fields
jeremy 50a239f
wrappers: BC5 forward-compat fields
jeremy 7bca136
wrappers: one-off structural fields
jeremy e4a6b94
wrappers: close remaining structural gaps surfaced by drift audit
jeremy 1ae18bc
scripts: field-level wrapper drift check
jeremy a83029e
wrappers: tests for new field propagation
jeremy 1c92e1e
wrappers: gofmt long-tag struct alignment
jeremy c2b1ed6
scripts: wrapper drift check verifies field population, not just tags
jeremy 3cd7608
wrappers: stop omitempty hiding false bools and zero timestamps
jeremy 5092743
wrappers: tighten propagation test assertions to exact values
jeremy f6e77ee
scripts: scope wrapper-drift population walk to the wrapper instance
jeremy b07a0af
wrappers: preserve flexible notification person ids on decode
jeremy ef4fcce
wrappers: preserve flexible gauge person ids; share embedded-person n…
jeremy 199795b
wrappers: drop omitempty from required visible_to_clients/inherits_st…
jeremy 1faff32
wrappers: test required-bool marshaling; fix stale normalize name in …
jeremy d777bdb
wrappers: clear golangci-lint findings in normalize.go
jeremy c0eeee7
scripts: enumerate complete direct-decode pair set
jeremy 0f550b5
wrappers: route subscription subscribers through personFromGenerated
jeremy a85f5f4
docs(brief): align omit-marker syntax + BubbleUpAt pointer choice
jeremy 7ae201b
scripts: extend wrapper-drift map with inline-converted tier
jeremy ebc5e47
scripts: extend wrapper-drift walker to population-check tier 3
jeremy 010cc49
scripts: regression tests for tier-3 composite-literal walker
jeremy a25995e
scripts: run wrapper-drift tests as part of the make target
jeremy 7116bd5
wrappers: extend required-bool serialization test to Inbox/Forward/Fo…
jeremy d4fe315
wrappers: delegate ProjectConstruction.Project to projectFromGenerated
jeremy File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,4 +3,5 @@ go 1.26 | |
| use ( | ||
| ./conformance/runner/go | ||
| ./go | ||
| ./scripts/check-wrapper-drift | ||
| ) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,207 @@ | ||
| # Brief: Surface BC5 forward-compat fields through Go hand-written wrappers | ||
|
|
||
| **Status**: blocker for `basecamp-cli` Phase 1 absorption. | ||
| **Audience**: SDK dev agent working from `basecamp-5-is-releasing-eager-spring.md`. | ||
| **Source**: filed by `basecamp-cli` `five` branch after attempting Phase 1 against SDK commit `7e9c4345` (Go regen of BC5 spec additions). | ||
|
|
||
| ## Summary | ||
|
|
||
| SDK PR 1 added BC5 forward-compat fields to the **generated** Go client | ||
| (`go/pkg/generated/client.gen.go`), but did not propagate them through the | ||
| **hand-written wrappers** in `go/pkg/basecamp/`. The wrappers are the | ||
| public Go API; CLI / external Go consumers never see the generated types | ||
| directly. As a result, the new fields are dropped during the | ||
| `*FromGenerated` conversion step and Phase 1 of the CLI absorption plan | ||
| cannot deliver any user-visible value. | ||
|
|
||
| The CLI's andon cord — *"if the SDK lacks a Go service wrapper for a | ||
| generated endpoint, never call the raw generated client from CLI code"* — | ||
| applies here in spirit: the wrapper exists, but it discards fields the | ||
| spec now declares. The fix lives in the SDK, not the CLI. | ||
|
|
||
| ## Evidence | ||
|
|
||
| `generated.Todo` now has `Steps []CardStep` (per `7e9c4345`'s diff to | ||
| `client.gen.go:~1990`). The wrapper: | ||
|
|
||
| ```go | ||
| // go/pkg/basecamp/todos.go:543 | ||
| func todoFromGenerated(gt generated.Todo) Todo { | ||
| t := Todo{ | ||
| Status: gt.Status, | ||
| Title: gt.Title, | ||
| // … no Steps assignment … | ||
| } | ||
| // … | ||
| return t | ||
| } | ||
| ``` | ||
|
|
||
| `Todo` (`go/pkg/basecamp/todos.go:17`) has no `Steps` field. CLI builds | ||
| fail when referencing `todo.Steps` after `app.Account().Todos().Get(...)`. | ||
|
|
||
| The same gap exists for every Phase-1 surface the CLI plan calls out: | ||
|
|
||
| | Generated type (after PR 1) | New fields in `generated/client.gen.go` | Hand-written wrapper file | Wrapper exposes them? | | ||
| |---|---|---|---| | ||
| | `generated.Todo` | `Steps []CardStep` | `pkg/basecamp/todos.go` `Todo` (line 17) | **no** | | ||
| | `generated.Todoset` | `TodosCount`, `CompletedLooseTodosCount`, `TodosUrl`, `AppTodosUrl` | `pkg/basecamp/todosets.go` `Todoset` | **no** | | ||
| | `generated.Person` | `Tagline` (alongside existing `Bio`) | `pkg/basecamp/todos.go` `Person` (line 50) | **no** | | ||
| | `generated.Notification` | `BubbleUpUrl`, `BubbleUpAt` | `pkg/basecamp/my_notifications.go` `Notification` (line 13) | **no** | | ||
| | `generated.GetMyNotificationsResponseContent` | `BubbleUps []Notification`, `ScheduledBubbleUps []Notification` | `pkg/basecamp/my_notifications.go` `NotificationsResult` (line 38) | **no** | | ||
|
|
||
| (There are likely more — these are just the ones the CLI Phase 1 hits | ||
| first. A wider audit comparing every `*FromGenerated` function against | ||
| its generated source after `7e9c4345` would be welcome.) | ||
|
|
||
| ## Why this happened | ||
|
|
||
| The SDK plan §1 ("Smithy spec — forward-compat additions") prescribes | ||
| the spec edits and PR 1 ships their regenerated client code. There is no | ||
| explicit step in the plan that says *"after regen, propagate new fields | ||
| through the hand-written `*FromGenerated` wrappers."* Implicit because | ||
| the Go SDK is the only one with this layer, easy to miss when the rest | ||
| of the SDK family is purely generated. | ||
|
|
||
| `SPEC.md` documents the architecture ("Go demonstrates the hand-written | ||
| service wrapper pattern"), and `CONTRIBUTING.md` mentions the | ||
| `go-check-drift` target — that target verifies *all generated operations | ||
| are covered by hand-written services*, but does **not** verify that | ||
| every generated *field* on covered structures is propagated. The current | ||
| drift check is operation-level, not field-level. | ||
|
|
||
| ## Contract / acceptance criteria | ||
|
|
||
| For each generated field added in PR 1, do exactly one of: | ||
|
|
||
| 1. **Add a corresponding field on the wrapper struct** with the same | ||
| semantic meaning, JSON tag matching the wire format, and propagate | ||
| it inside the relevant `*FromGenerated` conversion. Default to this | ||
| for fields the CLI / external consumers will want to read. | ||
| 2. **Document the omission inline** with an `// intentionally-omitted: | ||
| <tag> - <reason>` comment anywhere inside the wrapper struct's | ||
| declaration block, if a field is genuinely not appropriate for the | ||
| public Go surface (e.g. an internal echo). `<tag>` is the JSON tag of | ||
| the omitted field on the GENERATED struct (e.g. `hidden`, not | ||
| `Hidden`); `<reason>` is required and free-form. The drift check | ||
| (`scripts/check-wrapper-drift`) recognises only this exact marker | ||
| form — see its `markerRe` regexp for the canonical syntax. Phase 1 | ||
| fields should not need this — they're all user-visible. | ||
|
|
||
| Per-type concrete proposals (signatures, no behavior change for | ||
| existing fields): | ||
|
|
||
| ```go | ||
| // pkg/basecamp/todos.go — Todo | ||
| type Todo struct { | ||
| // … existing fields … | ||
| Steps []CardStep `json:"steps,omitempty"` | ||
| } | ||
|
|
||
| func todoFromGenerated(gt generated.Todo) Todo { | ||
| t := Todo{ /* existing assignments */ } | ||
| if len(gt.Steps) > 0 { | ||
| t.Steps = make([]CardStep, 0, len(gt.Steps)) | ||
| for _, gs := range gt.Steps { | ||
| t.Steps = append(t.Steps, cardStepFromGenerated(gs)) | ||
| } | ||
| } | ||
| return t | ||
| } | ||
| ``` | ||
|
|
||
| ```go | ||
| // pkg/basecamp/todosets.go — Todoset | ||
| type Todoset struct { | ||
| // … existing fields … | ||
| TodosCount int `json:"todos_count"` // BC5 | ||
| CompletedLooseTodosCount int `json:"completed_loose_todos_count"` // BC5 | ||
| TodosURL string `json:"todos_url,omitempty"` // BC5 | ||
| AppTodosURL string `json:"app_todos_url,omitempty"` // BC5 | ||
| } | ||
| ``` | ||
|
|
||
| ```go | ||
| // pkg/basecamp/todos.go — Person | ||
| type Person struct { | ||
| // … existing fields, including Bio … | ||
| Tagline string `json:"tagline,omitempty"` // BC5; alias of Bio per spec note | ||
| } | ||
| ``` | ||
|
|
||
| ```go | ||
| // pkg/basecamp/my_notifications.go | ||
| type Notification struct { | ||
| // … existing fields … | ||
| BubbleUpURL string `json:"bubble_up_url,omitempty"` // BC5 | ||
| BubbleUpAt *time.Time `json:"bubble_up_at,omitempty"` // BC5 | ||
| } | ||
|
|
||
| type NotificationsResult struct { | ||
| // … existing fields, including Memories … | ||
| BubbleUps []Notification `json:"bubble_ups,omitempty"` // BC5 | ||
| ScheduledBubbleUps []Notification `json:"scheduled_bubble_ups,omitempty"` // BC5 | ||
| } | ||
| ``` | ||
|
|
||
| For `Notification.BubbleUpAt`, use `*time.Time` rather than the bare | ||
| `time.Time` pattern the wrapper uses for `ReadAt` / `UnreadAt`. The | ||
| pointer is what shipped (see `my_notifications.go`): a bare `time.Time` | ||
| with `omitempty` still marshals the zero time as | ||
| `"0001-01-01T00:00:00Z"` (Go's `omitempty` does not suppress structs | ||
| that are merely zero-valued), so the absence of a scheduled bubble-up | ||
| would surface as a wrong wire value rather than an omitted key. The | ||
| `*time.Time` convention mirrors `Card.CompletedAt` and `Todo.CompletedAt`, | ||
| where the same omit-cleanly semantics matter. Consumers should | ||
| `nil`-check or use the `time.Time` returned by dereferencing rather than | ||
| calling `IsZero()`. | ||
|
|
||
| ## Verification | ||
|
|
||
| A field-level drift check would prevent recurrence. Sketch: | ||
|
|
||
| - Walk every wrapper struct in `pkg/basecamp/*.go`. | ||
| - Locate the corresponding generated struct (by either explicit | ||
| conversion func or by name match). | ||
| - For each generated field, fail if the wrapper has no field with a | ||
| matching JSON tag *and* no `// intentionally-omitted: ...` marker on | ||
| the wrapper struct. | ||
|
|
||
| This is a separate hardening PR; not blocking the immediate wrapper | ||
| update. | ||
|
|
||
| For the immediate PR, manual verification is enough: | ||
|
|
||
| ``` | ||
| go build ./... # cli & sdk both clean | ||
| go test ./pkg/basecamp/... # existing wrapper tests pass | ||
| make go-check-drift # current drift check passes | ||
| ``` | ||
|
|
||
| Then the CLI side runs: | ||
|
|
||
| ``` | ||
| make bump-sdk REF=<wrapper-PR-merge-commit> | ||
| go build ./... # cli compiles with new fields | ||
| go test ./... # cli tests still pass | ||
| ``` | ||
|
|
||
| ## Out of scope for this brief | ||
|
|
||
| - Other-language SDK behavior. TS / Ruby / Python / Swift / Kotlin | ||
| consume generated types directly (per `AGENTS.md`: "No hand-written | ||
| API methods exist in any SDK runtime"); those SDKs picked up the new | ||
| fields automatically when their generators ran. | ||
| - Spec changes. Spec is correct; this is purely a Go-layer omission. | ||
| - Recording.Bubbleupable. Per the CLI plan, that field tracks Phase 3e | ||
| (`recording-bubbleupable-field` brief, currently `no-json-contract`) | ||
| rather than Phase 1. | ||
|
|
||
| ## Why now | ||
|
|
||
| `basecamp-cli` Phase 1 is the first downstream consumer to attempt | ||
| absorbing the BC5 forward-compat fields. The Phase 1 work is presenter-only | ||
| on paper and could ship in a single commit per surface — but every | ||
| surface is gated on the wrapper exposing the field. CLI work will resume | ||
| the moment a wrapper-update PR lands; the bump-sdk → presenter changes | ||
| chain is well understood. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.