Skip to content

BC5 readiness: Go wrapper forward-compat audit + field-level drift check#309

Open
jeremy wants to merge 8 commits into
bc5-readiness-spec-foundationsfrom
bc5-readiness-go-wrappers
Open

BC5 readiness: Go wrapper forward-compat audit + field-level drift check#309
jeremy wants to merge 8 commits into
bc5-readiness-spec-foundationsfrom
bc5-readiness-go-wrappers

Conversation

@jeremy
Copy link
Copy Markdown
Member

@jeremy jeremy commented May 16, 2026

Summary

The Go SDK is unique among 37signals' SDKs in having hand-written wrapper structs in go/pkg/basecamp/ that re-shape the generated types from go/pkg/generated/. PR 1 (#293) regenerated the generated Go client with BC5 forward-compat fields, but the wrappers were not updated — so external consumers (basecamp-cli Phase 1) couldn't see the new fields. A field-level audit (filed at go/BRIEF-bc5-forward-compat-wrappers.md by basecamp-cli's dev agent) revealed the gap predates BC5: ~65 fields across 22 wrapper structs were missing.

This PR closes the gap on three scope categories and installs a field-level drift check to prevent recurrence.

Stacked on: PR #293 (bc5-readiness-spec-foundations) because the generated fields in commit 7e9c4345 are inputs the wrapper propagation depends on. Will retarget to main after #293 merges.

Downstream impact: unblocks basecamp-cli Phase 1, which is blocked on this PR.

Scope

Category A — BC5 forward-compat (9 fields, original brief scope)

  • Todo.Steps []CardStep
  • Todoset.{TodosCount, CompletedLooseTodosCount, TodosURL, AppTodosURL}
  • Person.Tagline (alongside existing Bio — surfaced as a separate wrapper field per the spec note)
  • Notification.{BubbleUpURL, BubbleUpAt} (time.Time matching the existing ReadAt/UnreadAt pattern)
  • NotificationsResult.{BubbleUps, ScheduledBubbleUps}

Category B — Cross-cutting structural (~50 fields, pre-existing gaps)

Pervasive Recording-shaped fields (BookmarkURL, BoostsCount, BoostsURL, CommentsCount, CommentsURL, SubscriptionURL, InheritsStatus, VisibleToClients, Title) added to Recording, Comment, Message, MessageBoard, Forward, ForwardReply, CampfireLine, ScheduleEntry, QuestionAnswer, Todolist, TimesheetEntry, Inbox. One-offs: Campfire.{Topic, Position, BookmarkURL, SubscriptionURL}, MessageBoard.{AppMessagesURL, Position}, Template.{URL, AppURL, Dock []DockItem} (reusing existing DockItem), Person.{CanPing, CanAccessHillCharts, CanAccessTimesheet} (CanPing field existed; propagation didn't), Notification.{Creator, Participants, PreviewableAttachments} (new PreviewableAttachment companion type), Gauge.{Creator, Bucket}, GaugeNeedle.{Creator, Bucket, Parent}, Inbox.{Position, ForwardsCount, ForwardsURL}, Forward.{RepliesCount, RepliesURL}, plus Card-table fields surfaced by the drift audit: Card.BoostsURL, CardColumn.CommentsCount, CardStep.CompletionURL, Document.BoostsURL, Event.{BoostsCount, BoostsURL}, Todo.{VisibleToClients, SubscriptionURL, BoostsURL, CommentsCount, CommentsURL, CompletionURL, CompletionSubscribers}, Upload.BoostsURL.

Category C — Structural pattern fixes

  1. Standardize nested Person on personFromGenerated — every *FromGenerated that built a nested *Person previously inlined a 6-field shallow copy that silently dropped Bio, Tagline, Location, Title, PersonableType, AttachableSGID, Client, Employee, TimeZone, CanPing, CanAccessHillCharts, CanAccessTimesheet, CanManageProjects, CanManagePeople, Company, CreatedAt, UpdatedAt. Replaced with personFromGenerated calls. Pure refactor; makes commit 3's Person.Tagline flow through every nested context for free.
  2. Direct-decode wrappersNotification, NotificationsResult, MyAssignment, Gauge, GaugeNeedle decode via json.Unmarshal on raw bytes. Added the missing fields with the appropriate JSON tags; decode pattern unchanged.

Cadence (8 commits)

Each commit individually builds and passes existing tests. Ordering chosen so the drift check (commit 5) goes green at introduction.

  1. wrappers: standardize nested Person on personFromGenerated
  2. wrappers: cross-cutting Recording-shaped fields
  3. wrappers: BC5 forward-compat fields
  4. wrappers: one-off structural fields
  5. (audit-driven) wrappers: close remaining structural gaps surfaced by drift audit
  6. scripts: field-level wrapper drift check
  7. wrappers: tests for new field propagation
  8. wrappers: gofmt long-tag struct alignment

Drift check

scripts/check-wrapper-drift/ (new), wired into make go-check-wrapper-drift and make check. Discovery is signature-based on *FromGenerated declarations plus an explicit map for the 5 direct-decode wrappers. Matching is keyed on JSON tag (not Go field name), so shape-equivalent collisions (URL vs Url, both tagged "url") are handled correctly. Opt-out marker format: // intentionally-omitted: <tag> - <reason> (ASCII hyphen + reason required, validated by regex). On the fully-fixed tree:

  • 51 (wrapper, generated) pairs walked
  • 842 generated fields verified
  • 0 drift, 0 intentionally-omitted markers required

Self-tests in scripts/check-wrapper-drift/main_test.go exercise the parser, marker regex, exclusion logic, and an end-to-end happy/drift case.

Verification

Gate Result
cd go && go build ./... clean
cd go && go test ./pkg/basecamp/... all pass (21 new propagation tests)
make go-check-drift (operation-level) 196 / 203 operations (96%), no drift
make go-check-wrapper-drift (NEW field-level) 51 pairs, 842 fields, no drift
make conformance-go 68 / 0 / 0 (unchanged from baseline)
make check all checks passed

make conformance-go-replay is not applicable on this branch (the replay target is part of PR 3, on a separate branch).

Risks + open questions

  • Untyped JSON marshalling assertions in downstream consumers. Nested Person values gain ~17 fields. Any user asserting JSONEq against the previously-truncated wrapper output sees additions. Mitigation: this is forward-compat additive (no removals or renames); basecamp-cli's bump-PR is well-understood.
  • Person.{CreatedAt, UpdatedAt} in nested contexts. personFromGenerated formats these as RFC3339 strings; nested contexts now also populate them when present (previously dropped). Forward-compat additive.
  • Shape-equivalent tag collisions. The drift check matches by JSON tag, not Go field name. Documented in scripts/check-wrapper-drift/main.go header.
  • Untouched MyAssignment Date semantics. StartsOn/DueOn already used string (matching Todo.DueOn); no change needed.

Coordination

basecamp-cli Phase 1 is blocked on this PR. Please notify the basecamp-cli five branch on merge so they can make bump-sdk and resume.


Summary by cubic

Propagates BC5 fields through the Go wrapper layer and closes wrapper-vs-generated field drift. Unblocks basecamp-cli Phase 1 by making the new fields available to consumers.

  • New Features

    • Surface BC5 fields across wrappers: Todo.Steps, Todoset.{TodosCount, CompletedLooseTodosCount, TodosURL, AppTodosURL}, Person.Tagline, Notification.{BubbleUpURL, BubbleUpAt}, NotificationsResult.{BubbleUps, ScheduledBubbleUps}.
    • Fill structural gaps across go/pkg/basecamp/: add pervasive fields like BookmarkURL, BoostsCount, BoostsURL, CommentsCount, CommentsURL, SubscriptionURL, InheritsStatus, VisibleToClients, Title, plus one-offs (e.g., Template.Dock, MessageBoard.Position, Inbox.{ForwardsCount, ForwardsURL}, Forward.{RepliesCount, RepliesURL}, Event.{BoostsCount, BoostsURL}).
    • Add a field-level drift check scripts/check-wrapper-drift with make go-check-wrapper-drift and make check to prevent regressions; includes self-tests and coverage for direct-decode wrappers.
  • Refactors

    • Standardize all nested Person mappings on personFromGenerated so full person fields flow in every context.
    • Keep direct-decode wrappers (Notification, NotificationsResult, MyAssignment, Gauge, GaugeNeedle) decoding pattern; extend structs with JSON tags to expose new fields.

Written for commit 9a6af51. Summary will update on new commits. Review in cubic

jeremy added 8 commits May 16, 2026 10:15
Every *FromGenerated function that built a nested *Person value (Creator,
Completer, Approver, Booster, Participants, top-level Person) previously
inlined a 6-field shallow copy that silently dropped Bio, Location, Title,
PersonableType, AttachableSGID, Client, Employee, TimeZone, CanPing,
CanAccessHillCharts, CanAccessTimesheet, CanManageProjects, CanManagePeople,
Company, CreatedAt, and UpdatedAt on every nested context.

Replace each shallow copy with a personFromGenerated() call. Pure refactor;
zero field additions on the wrapper struct side. The change is forward-compat
additive — fields that were dropped before are now populated when present in
the generated payload; no removals.

This makes every subsequent additive Person field (e.g. Tagline) flow through
every nested context for free, instead of requiring per-context re-adds.
Add the pervasive Category B fields (BookmarkURL, BoostsCount, BoostsURL,
CommentsCount, CommentsURL, SubscriptionURL, InheritsStatus, VisibleToClients,
Title) — and Content where applicable — to the wrappers that already share
the Recording shape but were missing one or more.

Affected wrappers:
  - Recording: + Content, CommentsCount, CommentsURL, SubscriptionURL
  - Comment: + VisibleToClients, Title, InheritsStatus, BookmarkURL,
    BoostsCount, BoostsURL
  - Message: + VisibleToClients, Title, InheritsStatus, BookmarkURL,
    BoostsURL, CommentsCount, CommentsURL, SubscriptionURL
  - MessageBoard: + VisibleToClients, InheritsStatus, BookmarkURL
  - Forward: + VisibleToClients, Title, InheritsStatus, BookmarkURL,
    SubscriptionURL
  - ForwardReply: + VisibleToClients, Title, InheritsStatus, BookmarkURL,
    BoostsCount, BoostsURL
  - Inbox: + VisibleToClients, InheritsStatus, BookmarkURL
  - CampfireLine: + BookmarkURL, BoostsURL
  - ScheduleEntry: + BoostsCount, BoostsURL
  - QuestionAnswer: + BoostsCount, BoostsURL
  - Todolist: + BoostsCount, BoostsURL
  - TimesheetEntry: + Status, VisibleToClients, Title, InheritsStatus, Type,
    URL, AppURL, BookmarkURL

Forward-compat additive — fields default to zero/empty when absent. Wrapper
JSON serialization uses omitempty on newly-added fields to match the
upstream serialization behavior.
Surface the BC5 forward-compat fields filed in
go/BRIEF-bc5-forward-compat-wrappers.md through the hand-written wrapper
layer. PR 1 (#293) added these on the generated client; this commit closes
the wrapper gap so basecamp-cli Phase 1 can read them.

  - Todo: + Steps []CardStep (BC5 jbuilder reuses the steps/step partial,
    matching the existing CardStep shape; reuse rather than introduce a
    new type)
  - Todoset: + TodosCount, CompletedLooseTodosCount, TodosURL, AppTodosURL
  - Person: + Tagline (alias of Bio; surfaced as a separate wrapper field
    per the spec note so consumers can distinguish presence)
  - Notification: + BubbleUpURL, BubbleUpAt (time.Time, following the
    existing ReadAt/UnreadAt zero-value pattern)
  - NotificationsResult: + BubbleUps, ScheduledBubbleUps

These wrappers all decode through the existing FromGenerated paths (Todo,
Todoset, Person) or direct JSON unmarshal (Notification, NotificationsResult)
with struct tags; no new conversion pattern is needed.

Refs: go/BRIEF-bc5-forward-compat-wrappers.md
Close the remaining Category B one-off structural gaps identified in the
field-level wrapper audit (predates BC5, surfaced during the BC5 readiness
sweep).

  - Campfire: + Topic, Position, BookmarkURL, SubscriptionURL
  - MessageBoard: + AppMessagesURL, Position
  - Template: + URL, AppURL, Dock []DockItem (reuses the existing DockItem
    type from projects.go; conversion shared via dockItemFromGenerated helper)
  - Person: + CanAccessHillCharts, CanAccessTimesheet; also wire CanPing into
    personFromGenerated (struct field existed; propagation did not)
  - Notification: + Creator, Participants, PreviewableAttachments (with new
    companion PreviewableAttachment type mirroring generated.PreviewableAttachment)
  - Gauge: + Creator, Bucket
  - GaugeNeedle: + Creator, Bucket, Parent
  - Inbox: + Position, ForwardsCount, ForwardsURL
  - Forward: + RepliesCount, RepliesURL

The Notification sentinel-creator test fixture was updated to include
`personable_type: "LocalPerson"` (the actual BC3 wire shape for system
actors) so normalizeJSON correctly coerces the string "basecamp" id to 0
and preserves it as system_label; the test now asserts the wrapper exposes
the normalized creator end-to-end rather than silently dropping it.
The field-level audit (formalized as the new check-wrapper-drift script
in the next commit) surfaced these additional pre-existing structural
gaps that the explicit lists in the prior commits did not cover.

  - Card: + BoostsURL (sibling of existing BoostsCount)
  - CardColumn: + CommentsCount (alongside the existing oddly-named
    CommentCount whose json tag was `comment_count`; the canonical
    `comments_count` tag now also flows through)
  - CardStep: + CompletionURL
  - Document: + BoostsURL
  - Event: + BoostsCount, BoostsURL
  - Todo: + VisibleToClients, SubscriptionURL, BoostsURL, CommentsCount,
    CommentsURL, CompletionURL, CompletionSubscribers []Person
  - Upload: + BoostsURL

All additions follow the same forward-compat additive pattern: zero/empty
when absent on the wire, populated through the existing *FromGenerated
conversion paths.
Adds a Go AST-based field-level drift check, sibling of the existing
operation-level scripts/check-service-drift.sh. The existing check confirms
that every generated operation has a hand-written service wrapper; this new
check confirms that every generated *struct field* is propagated through the
hand-written wrapper layer (or explicitly marked as intentionally-omitted).

Discovery: walks (wrapper, generated) pairs in two ways —
  1. signature reading of every `xFromGenerated(g generated.Y) X` function
     declaration in go/pkg/basecamp/*.go (non-test).
  2. an explicit `directDecodePairs` map covering wrappers that decode
     straight from JSON bytes (Notification, NotificationsResult,
     MyAssignment, Gauge, GaugeNeedle) without a *FromGenerated function.

Matching is keyed on JSON tag, not Go field name, so shape-equivalent
collisions (`URL string \`json:"url"\`` vs `Url string \`json:"url"\``) are
handled correctly. Opt-out marker format
  // intentionally-omitted: <tag> - <reason>
(ASCII hyphen + space + reason, validated by regex) can appear anywhere
inside the wrapper struct body.

Wired into:
  - new `make go-check-wrapper-drift` target
  - existing `make check` aggregate (next to `go-check-drift`)
  - go.work `use` block, so `go run ./scripts/check-wrapper-drift/`
    works without extra setup.

Self-tests in main_test.go exercise the parser/extractor helpers, the
marker regex (reason-required), the excluded-FromGenerated exception
(webhookPersonFromGenerated → WebhookEventPerson), and a happy/drift
end-to-end pair.

Result on the fully-fixed tree: 51 (wrapper, generated) pairs walked,
842 generated fields verified, zero drift, zero intentionally-omitted
markers required.
Adds wrapper_propagation_test.go with three test patterns:

  1. Test<Type>FromGenerated_PropagatesNewFields — one per wrapper file
     that gained fields. Asserts only the new fields against a
     fully-populated generated.X fixture; the existing
     *FromGenerated_FullPopulated tests still cover the full surface.

  2. TestPerson_NewFields_NestedPropagation — end-to-end check that the
     personFromGenerated standardization causes a fully-populated nested
     generated.Person (Bio, Tagline, Location, Title, AttachableSGID,
     PersonableType, Client, Employee, TimeZone, CanPing,
     CanAccessHillCharts, CanAccessTimesheet, CanManageProjects,
     CanManagePeople, Company, CreatedAt, UpdatedAt) to flow through
     every nested context (Recording, Comment, Message, Todo,
     ScheduleEntry.Creator + Participants, Card.Creator + Completer +
     Assignees). Pins commit 1's standardization end-to-end.

  3. Test<Type>_DirectDecode_PropagatesNewFields — JSON-fixture tests for
     the §C-2 direct-decode wrappers (Notification, NotificationsResult,
     MyAssignment, Gauge, GaugeNeedle) that decode via json.Unmarshal on
     the (sometimes pre-normalized) raw body.

Also commits the original brief at go/BRIEF-bc5-forward-compat-wrappers.md
that triggered this PR — filed by the basecamp-cli dev agent against
SDK commit 7e9c434. Preserving it in-tree keeps the audit trail of why
Category A field additions look the way they do, alongside the wider
audit (Categories B + C) that the brief flagged would be welcome.

21 new tests in total; all pass on the current tree.
`gofmt -s -w` rewrites the column-aligned tag layouts in Notification,
NotificationsResult, Person, Todo, and Todoset where the newly-added fields
pushed the existing alignment past gofmt's preferred width. No behavior
change; closes `make fmt-check` (and thus `make check`).
Copilot AI review requested due to automatic review settings May 16, 2026 17:48
@github-actions github-actions Bot added dependencies Pull requests that update a dependency file go labels May 16, 2026
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR updates the Go SDK’s hand-written wrapper layer (go/pkg/basecamp/) to surface fields that already exist on the regenerated generated client types (go/pkg/generated/), and adds an automated field-level drift check to prevent wrapper-vs-generated mismatches from recurring (BC5 readiness / forward-compat).

Changes:

  • Propagate missing generated fields into Go wrapper structs and their *FromGenerated conversions (including standardizing nested Person mapping via personFromGenerated).
  • Add scripts/check-wrapper-drift (with unit tests) and wire it into make check via go-check-wrapper-drift.
  • Add wrapper propagation tests to pin the newly surfaced fields (including coverage for “direct-decode” wrappers that unmarshal JSON bytes).

Tip

If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.

Reviewed changes

Copilot reviewed 36 out of 36 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
scripts/check-wrapper-drift/main.go Implements field-level drift detection between generated structs and Go wrappers (by JSON tag).
scripts/check-wrapper-drift/main_test.go Self-tests for tag parsing, marker parsing, exclusions, and end-to-end drift detection.
scripts/check-wrapper-drift/go.mod Declares a small Go module for the drift checker so it can be run/tested cleanly.
Makefile Adds go-check-wrapper-drift and runs it as part of make check.
go.work Adds the drift-checker module to the workspace.
go/pkg/basecamp/wrapper_propagation_test.go New tests asserting the newly surfaced fields actually propagate through wrappers (incl. direct-decode structs).
go/pkg/basecamp/people.go Expands personFromGenerated to include newly surfaced Person fields (e.g., Tagline, access flags).
go/pkg/basecamp/todos.go Adds BC5 and structural fields to Todo + extends todoFromGenerated (steps, URLs, counts, subscribers) and expands Person wrapper.
go/pkg/basecamp/todosets.go Adds BC5 todoset count/URL fields and propagates them from generated.
go/pkg/basecamp/my_notifications.go Adds BC5 notification bubble-up fields plus creator/participants/attachments on direct-decode wrapper structs.
go/pkg/basecamp/my_notifications_test.go Updates sentinel-creator test now that Notification.Creator is exposed by the wrapper.
go/pkg/basecamp/recordings.go Surfaces recording-shaped fields (content/comments/subscription) in wrapper + conversion.
go/pkg/basecamp/comments.go Surfaces recording-shaped fields in Comment wrapper + conversion.
go/pkg/basecamp/messages.go Surfaces recording-shaped fields in Message wrapper + conversion.
go/pkg/basecamp/message_boards.go Adds structural fields (bookmark/app messages URL/position/visibility flags) to wrapper + conversion.
go/pkg/basecamp/campfires.go Adds missing campfire + line fields (topic/position/bookmark/subscription/boosts URL) and standardizes nested Person.
go/pkg/basecamp/schedules.go Adds boosts fields and standardizes nested Person + participants mapping.
go/pkg/basecamp/checkins.go Adds boosts fields and standardizes nested Person mappings in questionnaire/question/answer wrappers.
go/pkg/basecamp/todolists.go Adds boosts fields and standardizes nested Person in todolist wrapper.
go/pkg/basecamp/todolist_groups.go Standardizes nested Person in todolist group wrapper.
go/pkg/basecamp/timesheet.go Expands TimesheetEntry wrapper to recording-shaped fields and standardizes nested Person mappings.
go/pkg/basecamp/forwards.go Expands Inbox/Forward/ForwardReply wrappers with recording-shaped fields + forwards/replies counters and standardizes nested Person.
go/pkg/basecamp/events.go Adds boosts fields and standardizes nested Person in event wrapper.
go/pkg/basecamp/vaults.go Adds boosts URL fields and standardizes nested Person for vault/document/upload.
go/pkg/basecamp/templates.go Adds URL/AppURL/Dock to template wrapper and propagates Dock items.
go/pkg/basecamp/projects.go Refactors DockItem conversion into shared dockItemFromGenerated.
go/pkg/basecamp/cards.go Adds boosts URL + card-step completion URL + column comments_count, and standardizes nested Person mappings in card types.
go/pkg/basecamp/gauges.go Surfaces creator/bucket/parent on direct-decode gauge wrappers.
go/pkg/basecamp/search.go Standardizes nested Person in search result wrapper.
go/pkg/basecamp/reports.go Standardizes nested Person in reports wrapper.
go/pkg/basecamp/timeline.go Standardizes nested Person in timeline wrapper.
go/pkg/basecamp/client_approvals.go Standardizes nested Person mapping for creator/approver/response creator.
go/pkg/basecamp/client_correspondences.go Standardizes nested Person mapping for creator.
go/pkg/basecamp/client_replies.go Standardizes nested Person mapping for creator.
go/pkg/basecamp/boosts.go Standardizes nested Person mapping for booster.
go/BRIEF-bc5-forward-compat-wrappers.md Adds/updates the brief documenting the wrapper-forward-compat motivation and acceptance criteria.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +173 to +176
for k, v := range collectFromGeneratedPairs(f) {
if !excludedFromGenerated[k+"FromGenerated"] && !excludedFromGenerated[lowercaseFirst(k)+"FromGenerated"] {
fromGenPairs[k] = v
}
Comment on lines +770 to +774
// -----------------------------------------------------------------------------
// types helper (silences unused-import lint when go test rebuilds)
// -----------------------------------------------------------------------------

var _ = types.Date{}
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

9 issues found across 36 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="Makefile">

<violation number="1" location="Makefile:226">
P3: Quote or remove the `cd $(CURDIR)` step in the wrapper drift target; unquoted `$(CURDIR)` can fail in paths with spaces.</violation>
</file>

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

<violation number="1" location="go/pkg/basecamp/my_notifications.go:35">
P2: `omitempty` does not suppress zero `time.Time` values here, so an unset `bubble_up_at` will marshal as `0001-01-01T00:00:00Z` instead of disappearing.</violation>
</file>

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

<violation number="1" location="go/pkg/basecamp/timesheet.go:16">
P2: Remove `omitempty` from the new bool flags. A legitimate `false` value currently disappears when `TimesheetEntry` is marshaled, so callers won't see these BC5 fields unless they happen to be `true`.</violation>
</file>

<file name="scripts/check-wrapper-drift/main_test.go">

<violation number="1" location="scripts/check-wrapper-drift/main_test.go:182">
P2: This "end-to-end" test reimplements only part of the drift check, so regressions in `run()`'s direct-decode pair handling or omitted-marker validation can slip through unnoticed.</violation>
</file>

<file name="scripts/check-wrapper-drift/main.go">

<violation number="1" location="scripts/check-wrapper-drift/main.go:174">
P3: This exclusion check is dead code: `k` is the wrapper struct name (e.g. `WebhookEventPerson`), so `k+"FromGenerated"` produces `WebhookEventPersonFromGenerated` — which never matches the actual exclusion key `webhookPersonFromGenerated`. The `lowercaseFirst` variant yields `webhookEventPersonFromGenerated`, also no match. The exclusion already works correctly inside `collectFromGeneratedPairs` (which checks `fd.Name.Name`), so this outer guard is misleading. Remove it to avoid implying there are two exclusion paths when only the inner one is effective.</violation>

<violation number="2" location="scripts/check-wrapper-drift/main.go:219">
P1: This audit only checks wrapper struct tags, not whether `*FromGenerated` actually populates those fields, so future field additions can still silently stay zero-valued while the drift check passes.</violation>
</file>

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

<violation number="1" location="go/pkg/basecamp/wrapper_propagation_test.go:204">
P2: Compare nested `CreatedAt`/`UpdatedAt` against the expected formatted timestamps instead of only checking for non-empty strings.</violation>

<violation number="2" location="go/pkg/basecamp/wrapper_propagation_test.go:373">
P2: Use exact URL comparisons here; a wrong field mapping still passes as long as each wrapper field is non-empty.</violation>
</file>

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

<violation number="1" location="go/pkg/basecamp/forwards.go:62">
P2: `omitempty` on the new forward/inbox response fields will hide false/zero values when callers marshal these wrappers, so the added BC5 fields still drift from the API shape.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
Re-trigger cubic

sort.Strings(tags)

var missing []string
for _, tag := range tags {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: This audit only checks wrapper struct tags, not whether *FromGenerated actually populates those fields, so future field additions can still silently stay zero-valued while the drift check passes.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/check-wrapper-drift/main.go, line 219:

<comment>This audit only checks wrapper struct tags, not whether `*FromGenerated` actually populates those fields, so future field additions can still silently stay zero-valued while the drift check passes.</comment>

<file context>
@@ -0,0 +1,446 @@
+		sort.Strings(tags)
+
+		var missing []string
+		for _, tag := range tags {
+			totalFieldsChecked++
+			if wrap.tags[tag] {
</file context>

BubbleUpURL string `json:"bubble_up_url,omitempty"`
// BubbleUpAt is the BC5-added scheduled resurfacing time when this item is
// queued as a scheduled Bubble Up. Zero when there is no scheduled time.
BubbleUpAt time.Time `json:"bubble_up_at,omitempty"`
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: omitempty does not suppress zero time.Time values here, so an unset bubble_up_at will marshal as 0001-01-01T00:00:00Z instead of disappearing.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At go/pkg/basecamp/my_notifications.go, line 35:

<comment>`omitempty` does not suppress zero `time.Time` values here, so an unset `bubble_up_at` will marshal as `0001-01-01T00:00:00Z` instead of disappearing.</comment>

<file context>
@@ -11,34 +11,64 @@ import (
+	BubbleUpURL string `json:"bubble_up_url,omitempty"`
+	// BubbleUpAt is the BC5-added scheduled resurfacing time when this item is
+	// queued as a scheduled Bubble Up. Zero when there is no scheduled time.
+	BubbleUpAt             time.Time               `json:"bubble_up_at,omitempty"`
+	UnreadURL              string                  `json:"unread_url,omitempty"`
+	SubscriptionURL        string                  `json:"subscription_url,omitempty"`
</file context>

Comment on lines +16 to +18
VisibleToClients bool `json:"visible_to_clients,omitempty"`
Title string `json:"title,omitempty"`
InheritsStatus bool `json:"inherits_status,omitempty"`
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Remove omitempty from the new bool flags. A legitimate false value currently disappears when TimesheetEntry is marshaled, so callers won't see these BC5 fields unless they happen to be true.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At go/pkg/basecamp/timesheet.go, line 16:

<comment>Remove `omitempty` from the new bool flags. A legitimate `false` value currently disappears when `TimesheetEntry` is marshaled, so callers won't see these BC5 fields unless they happen to be `true`.</comment>

<file context>
@@ -11,17 +11,25 @@ import (
-	UpdatedAt      time.Time `json:"updated_at"`
+	ID               int64     `json:"id"`
+	Status           string    `json:"status,omitempty"`
+	VisibleToClients bool      `json:"visible_to_clients,omitempty"`
+	Title            string    `json:"title,omitempty"`
+	InheritsStatus   bool      `json:"inherits_status,omitempty"`
</file context>
Suggested change
VisibleToClients bool `json:"visible_to_clients,omitempty"`
Title string `json:"title,omitempty"`
InheritsStatus bool `json:"inherits_status,omitempty"`
VisibleToClients bool `json:"visible_to_clients"`
Title string `json:"title,omitempty"`
InheritsStatus bool `json:"inherits_status"`

}
genStructs := collectStructsAndMarkers(fset, genFile)
wrapStructs := collectStructsAndMarkers(fset, wrapFile)
pairs := collectFromGeneratedPairs(wrapFile)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: This "end-to-end" test reimplements only part of the drift check, so regressions in run()'s direct-decode pair handling or omitted-marker validation can slip through unnoticed.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/check-wrapper-drift/main_test.go, line 182:

<comment>This "end-to-end" test reimplements only part of the drift check, so regressions in `run()`'s direct-decode pair handling or omitted-marker validation can slip through unnoticed.</comment>

<file context>
@@ -0,0 +1,245 @@
+	}
+	genStructs := collectStructsAndMarkers(fset, genFile)
+	wrapStructs := collectStructsAndMarkers(fset, wrapFile)
+	pairs := collectFromGeneratedPairs(wrapFile)
+
+	// Foo: in sync (Hidden marked as intentionally-omitted).
</file context>

if !w.VisibleToClients || w.Title != "M" || !w.InheritsStatus {
t.Errorf("base recording-shaped fields not propagated: %+v", w)
}
if w.BookmarkURL == "" || w.BoostsURL == "" || w.CommentsURL == "" || w.SubscriptionURL == "" {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Use exact URL comparisons here; a wrong field mapping still passes as long as each wrapper field is non-empty.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At go/pkg/basecamp/wrapper_propagation_test.go, line 373:

<comment>Use exact URL comparisons here; a wrong field mapping still passes as long as each wrapper field is non-empty.</comment>

<file context>
@@ -0,0 +1,774 @@
+	if !w.VisibleToClients || w.Title != "M" || !w.InheritsStatus {
+		t.Errorf("base recording-shaped fields not propagated: %+v", w)
+	}
+	if w.BookmarkURL == "" || w.BoostsURL == "" || w.CommentsURL == "" || w.SubscriptionURL == "" {
+		t.Errorf("URL fields not propagated: %+v", w)
+	}
</file context>

Comment on lines +204 to +209
if p.CreatedAt == "" {
t.Error("CreatedAt: expected non-empty string from non-zero time")
}
if p.UpdatedAt == "" {
t.Error("UpdatedAt: expected non-empty string from non-zero time")
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Compare nested CreatedAt/UpdatedAt against the expected formatted timestamps instead of only checking for non-empty strings.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At go/pkg/basecamp/wrapper_propagation_test.go, line 204:

<comment>Compare nested `CreatedAt`/`UpdatedAt` against the expected formatted timestamps instead of only checking for non-empty strings.</comment>

<file context>
@@ -0,0 +1,774 @@
+			t.Errorf("Company.Name: got %q, want %q", p.Company.Name, gp.Company.Name)
+		}
+	}
+	if p.CreatedAt == "" {
+		t.Error("CreatedAt: expected non-empty string from non-zero time")
+	}
</file context>
Suggested change
if p.CreatedAt == "" {
t.Error("CreatedAt: expected non-empty string from non-zero time")
}
if p.UpdatedAt == "" {
t.Error("UpdatedAt: expected non-empty string from non-zero time")
}
if p.CreatedAt != gp.CreatedAt.Format("2006-01-02T15:04:05Z07:00") {
t.Errorf("CreatedAt: got %q, want %q", p.CreatedAt, gp.CreatedAt.Format("2006-01-02T15:04:05Z07:00"))
}
if p.UpdatedAt != gp.UpdatedAt.Format("2006-01-02T15:04:05Z07:00") {
t.Errorf("UpdatedAt: got %q, want %q", p.UpdatedAt, gp.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"))
}

Creator *Person `json:"creator,omitempty"`
ID int64 `json:"id"`
Status string `json:"status"`
VisibleToClients bool `json:"visible_to_clients,omitempty"`
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: omitempty on the new forward/inbox response fields will hide false/zero values when callers marshal these wrappers, so the added BC5 fields still drift from the API shape.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At go/pkg/basecamp/forwards.go, line 62:

<comment>`omitempty` on the new forward/inbox response fields will hide false/zero values when callers marshal these wrappers, so the added BC5 fields still drift from the API shape.</comment>

<file context>
@@ -57,48 +57,67 @@ type ForwardReplyListResult struct {
-	Creator   *Person   `json:"creator,omitempty"`
+	ID               int64     `json:"id"`
+	Status           string    `json:"status"`
+	VisibleToClients bool      `json:"visible_to_clients,omitempty"`
+	CreatedAt        time.Time `json:"created_at"`
+	UpdatedAt        time.Time `json:"updated_at"`
</file context>

Comment thread Makefile
# operation-level, this one is field-level.
go-check-wrapper-drift:
@echo "==> Checking wrapper field-level drift..."
@cd $(CURDIR) && go run ./scripts/check-wrapper-drift/
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: Quote or remove the cd $(CURDIR) step in the wrapper drift target; unquoted $(CURDIR) can fail in paths with spaces.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Makefile, line 226:

<comment>Quote or remove the `cd $(CURDIR)` step in the wrapper drift target; unquoted `$(CURDIR)` can fail in paths with spaces.</comment>

<file context>
@@ -218,6 +218,13 @@ go-check-drift:
+# operation-level, this one is field-level.
+go-check-wrapper-drift:
+	@echo "==> Checking wrapper field-level drift..."
+	@cd $(CURDIR) && go run ./scripts/check-wrapper-drift/
+
 .PHONY: auth-routable-check
</file context>
Suggested change
@cd $(CURDIR) && go run ./scripts/check-wrapper-drift/
@go run ./scripts/check-wrapper-drift/

wrapperStructs[k] = v
}
for k, v := range collectFromGeneratedPairs(f) {
if !excludedFromGenerated[k+"FromGenerated"] && !excludedFromGenerated[lowercaseFirst(k)+"FromGenerated"] {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: This exclusion check is dead code: k is the wrapper struct name (e.g. WebhookEventPerson), so k+"FromGenerated" produces WebhookEventPersonFromGenerated — which never matches the actual exclusion key webhookPersonFromGenerated. The lowercaseFirst variant yields webhookEventPersonFromGenerated, also no match. The exclusion already works correctly inside collectFromGeneratedPairs (which checks fd.Name.Name), so this outer guard is misleading. Remove it to avoid implying there are two exclusion paths when only the inner one is effective.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/check-wrapper-drift/main.go, line 174:

<comment>This exclusion check is dead code: `k` is the wrapper struct name (e.g. `WebhookEventPerson`), so `k+"FromGenerated"` produces `WebhookEventPersonFromGenerated` — which never matches the actual exclusion key `webhookPersonFromGenerated`. The `lowercaseFirst` variant yields `webhookEventPersonFromGenerated`, also no match. The exclusion already works correctly inside `collectFromGeneratedPairs` (which checks `fd.Name.Name`), so this outer guard is misleading. Remove it to avoid implying there are two exclusion paths when only the inner one is effective.</comment>

<file context>
@@ -0,0 +1,446 @@
+			wrapperStructs[k] = v
+		}
+		for k, v := range collectFromGeneratedPairs(f) {
+			if !excludedFromGenerated[k+"FromGenerated"] && !excludedFromGenerated[lowercaseFirst(k)+"FromGenerated"] {
+				fromGenPairs[k] = v
+			}
</file context>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file go

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants