Skip to content

Normalize bucketId as project scope in operation metadata (#422)#440

Merged
jeremy merged 8 commits into
mainfrom
feat/op-scope-bucketid
Jul 25, 2026
Merged

Normalize bucketId as project scope in operation metadata (#422)#440
jeremy merged 8 commits into
mainfrom
feat/op-scope-bucketid

Conversation

@jeremy

@jeremy jeremy commented Jul 25, 2026

Copy link
Copy Markdown
Member

Closes #422.

This corrects wrong per-operation metadata behavior (bug). The five service generators recognized project scope only from a path label literally named projectId, so bucketId-labeled operations (/{accountId}/buckets/{bucketId}/…) emitted projectId = null, and on the three accountId+bucketId-only paths the bucket id was additionally mis-emitted as resourceId. This normalizes bucketId as project scope in per-operation hook/observability metadata across all six SDKs. Surfaced during #402 (card-table wormholes) review and deferred as out of scope.

The SDK already treats the two labels as one scope in the Go URL parser (url.go:357) and in check-bucket-flat-parity.sh — the generators were the lone holdout. This is a consistency repair, not a new convention.

Per the scope decision on #422, this is full cross-SDK parity: the five generators are fixed, and the hand-written Go observability layer is brought to parity by adding a new exported OperationInfo.ProjectID field (Go previously had no project field at all).

What changed

Nine bucketId-labeled operationsSetCardColumnColor, EnableCardColumnOnHold, DisableCardColumnOnHold, CreateWormhole, UpdateWormhole, DeleteWormhole, ListWebhooks, CreateWebhook, CreateTool:

  • All nine now report projectId (= the bucket id).
  • Three collection ops (ListWebhooks, CreateWebhook, CreateTool) — the accountId+bucketId-only paths — change resourceId from the bucket id to null. The other six keep their deeper id (columnId/cardTableId/wormholeId).

Generators (one commit per SDK): match projectId or bucketId for project scope and emit the matched param's actual local name; exclude bucketId (as well as projectId) from resourceId selection.

Go hand-written layer: add exported OperationInfo.ProjectID, emit a basecamp.project_id otel span attribute, and populate ProjectID/ResourceID at all 19 project/bucket-scoped sites with the rule project/bucket id → ProjectID; deepest non-project id → ResourceID. This also relocates the mis-assigned bucket/project id out of ResourceID for Tools.Create and Projects.Get/Update/Trash, matching the generated SDKs.

Contract vs. API framing

  • Wire / API-provenance contract: unchanged. No Smithy edits, no spec/api-provenance.json bump. openapi.json, behavior-model.json, and url-routes.json regenerate byte-identical. Operation and service counts are unchanged (208 ops).
  • Public Go observability API: expanded (additive). See below.

Release notes / compatibility

  • Additive exported field: Go OperationInfo gains an exported ProjectID int64. Operations with a project/bucket path label now populate it, and the otel hook emits a basecamp.project_id span attribute alongside basecamp.resource_id.
  • resourceId/ResourceID semantics tightened for the three collection ops (ListWebhooks, CreateWebhook, CreateTool) and, in Go, for Tools.Create and Projects.Get/Update/Trash: a bucket/project id that was previously reported as the resource id is now reported as the project id (resource id becomes null/0). Consumers that keyed dashboards or filters off resourceId for these specific ops will see the value move to projectId.
  • Non-breaking disposition (basis): classified non-breaking under this repository's compatibility policy and golang.org/x/exp/apidiff. Adding OperationInfo.ProjectID is source-compatible except for unkeyed struct literals of OperationInfo — a case apidiff ignores by design. OperationInfo is an exported struct and can be constructed externally, but in practice consumers receive it through the hook callback rather than constructing it, so the unkeyed-literal break is not a realistic external pattern. Caveat preserved so a maintainer can escalate to the Breaking-Changes category if they judge otherwise.

Tests

  • Go: a table-driven test drives a real service call per op through a capturing hook and asserts the {ProjectID, ResourceID} split for every project/bucket-scoped op — mechanically guaranteeing no forgotten assignment. Existing tools_test.go and otel_test.go assertions updated for the new split.
  • Each generated SDK: real generated-service calls through the language's hook spy, covering both shapes — CreateWormhole (bucket becomes project scope, deeper resource id kept) and CreateTool/ListWebhooks (bucket becomes project scope, resource id null).

Reproducibility note

One commit absorbs a pre-existing swift/Sources/Basecamp/Generated/Metadata.swift regeneration artifact (adds DeleteWormhole/UpdateWormhole to the idempotent-operation list) left over from #402 and unrelated to project scope — there is no Swift drift gate, so it had gone uncommitted. Absorbing it keeps the branch reproducible: make generate now leaves the tree clean (modulo generation timestamps).

Verification

make check and make url-routes-check both green (conformance, drift checks, bucket-flat parity, api-gaps, deprecation parity). url-routes.json, openapi.json, behavior-model.json, and provenance regenerate byte-identical.

jeremy added 6 commits July 24, 2026 23:24
Match projectId or bucketId for the operation's project scope and emit the
matched path param, and exclude bucketId (as well as projectId) from resourceId
selection. The nine bucketId-labeled ops now report projectId; the three
collection ops (ListWebhooks, CreateWebhook, CreateTool) drop the bucket id from
resourceId. Preserve the projectId shorthand for projectId-labeled ops so their
generated output is byte-identical.
Match projectId or bucketId for project scope and emit the matched param's
snake_case name, and exclude bucketId (as well as projectId) from resource_id
selection. The nine bucketId-labeled ops now report project_id; the three
collection ops (ListWebhooks, CreateWebhook, CreateTool) drop the bucket id from
resource_id.
Match projectId or bucketId for project scope (emit is already dynamic), and
exclude bucketId (as well as projectId) from resource_id selection. The nine
bucketId-labeled ops now report project_id; the three collection ops
(ListWebhooks, CreateWebhook, CreateTool) drop the bucket id from resource_id.
Match projectId or bucketId for project scope and emit the matched param's
camelCase name (previously a hardcoded projectId, which would reference an
undefined local for a bucketId op), and exclude bucketId (as well as projectId)
from resourceId selection. The nine bucketId-labeled ops now report projectId;
the three collection ops (ListWebhooks, CreateWebhook, CreateTool) drop the
bucket id from resourceId.
Match projectId or bucketId for project scope and emit the matched param's name
(previously a hardcoded projectId, which would reference an undefined local for
a bucketId op), and exclude bucketId (as well as projectId) from resourceId
selection. The nine bucketId-labeled ops now report projectId; the three
collection ops (ListWebhooks, CreateWebhook, CreateTool) drop the bucket id from
resourceId.
Bring the hand-written Go observability layer to parity with the generated SDKs.
Add an exported OperationInfo.ProjectID field and populate it at all 19 sites
carrying a project/bucket id, with the rule: project/bucket id -> ProjectID,
deepest non-project id -> ResourceID. Emit a basecamp.project_id span attribute
in the otel hook. Relocate the mis-assigned bucket/project id out of ResourceID
for Tools.Create and Projects.Get/Update/Trash so they match the generated SDKs.

A table-driven test drives a real service call per op through a capturing hook
and asserts the {ProjectID, ResourceID} split, guaranteeing no forgotten
assignment. SPEC.md drops the '(Go omits this field)' clause on project_id.

Public Go observability API expansion (additive): OperationInfo gains an
exported ProjectID field. This can only break unkeyed struct literals of
OperationInfo, which consumers never construct (the struct is received via the
hook callback); apidiff ignores this case by design.
Copilot AI review requested due to automatic review settings July 25, 2026 06:26
@jeremy jeremy added enhancement New feature or request go labels Jul 25, 2026
@github-actions github-actions Bot added documentation Improvements or additions to documentation typescript Pull requests that update TypeScript code ruby Pull requests that update the Ruby SDK kotlin swift and removed documentation Improvements or additions to documentation labels Jul 25, 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.

Pull request overview

Normalizes bucketId as the project/bucket scope identifier in per-operation hook/observability metadata across all six SDKs, aligning generator behavior with existing Go URL parsing and eliminating cases where bucketId was incorrectly reported as resourceId.

Changes:

  • Generators (TS/Ruby/Python/Kotlin/Swift): treat bucketId as project scope (projectId/project_id) and exclude it from resourceId selection.
  • Generated services: populate projectId for the nine bucketId-scoped operations; clear resourceId for bucket-scoped collection operations (ListWebhooks, CreateWebhook, CreateTool).
  • Go observability: add OperationInfo.ProjectID, emit basecamp.project_id otel span attribute, and add a table-driven regression test pinning {ProjectID, ResourceID} across all project/bucket-scoped ops.

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 30 out of 50 changed files in this pull request and generated no comments.

Show a summary per file
File Description
typescript/tests/services/wormholes.test.ts Adds hook assertion for {projectId=bucketId, resourceId=cardTableId}.
typescript/tests/services/webhooks.test.ts Adds hook assertion for {projectId=bucketId, resourceId=undefined} on collection op.
typescript/src/generated/services/wormholes.ts Emits projectId: bucketId for wormhole ops.
typescript/src/generated/services/webhooks.ts Moves bucket from resourceId to projectId for list/create.
typescript/src/generated/services/tools.ts Moves bucket from resourceId to projectId for create.
typescript/src/generated/services/card-columns.ts Emits projectId: bucketId for card-column ops.
typescript/scripts/generate-services.ts Generator: bucketId recognized as project scope; excluded from resourceId.
swift/Tests/BasecampTests/WormholesServiceTests.swift Adds test asserting emitted projectId/resourceId for CreateWormhole.
swift/Tests/BasecampTests/GeneratedServiceTests.swift Adds test asserting ListWebhooks has projectId and nil resourceId.
swift/Sources/BasecampGenerator/ServiceEmitter.swift Generator: treat bucketId as project scope; exclude from resource selection.
swift/Sources/Basecamp/Generated/Services/WormholesService.swift Generated services now pass projectId: bucketId.
swift/Sources/Basecamp/Generated/Services/WebhooksService.swift Generated services now pass projectId: bucketId and omit resourceId.
swift/Sources/Basecamp/Generated/Services/ToolsService.swift Generated services now pass projectId: bucketId and omit resourceId.
swift/Sources/Basecamp/Generated/Services/CardColumnsService.swift Generated services now pass projectId: bucketId.
SPEC.md Updates OperationInfo contract to reflect project scope being present (no Go exception).
ruby/test/basecamp/services/wormholes_service_test.rb Adds hook-capture test asserting project_id/resource_id split.
ruby/test/basecamp/services/webhooks_service_test.rb Adds hook-capture test asserting project_id set and resource_id nil for list.
ruby/scripts/generate-services.rb Generator: treat bucketId as project scope; exclude from resource selection.
ruby/lib/basecamp/generated/services/wormholes_service.rb Emits project_id: bucket_id for wormhole ops.
ruby/lib/basecamp/generated/services/webhooks_service.rb Moves bucket from resource_id to project_id for list/create.
ruby/lib/basecamp/generated/services/tools_service.rb Moves bucket from resource_id to project_id for create.
ruby/lib/basecamp/generated/services/card_columns_service.rb Emits project_id: bucket_id for card-column ops.
python/tests/services/test_wormholes_service.py Adds hook-capture tests asserting project_id/resource_id split (sync + async).
python/tests/services/test_tools.py Adds hook-capture tests asserting project_id set and resource_id is None (sync + async).
python/src/basecamp/generated/services/wormholes.py Emits project_id=bucket_id for wormhole ops.
python/src/basecamp/generated/services/webhooks_service.py Moves bucket from resource_id to project_id for list/create.
python/src/basecamp/generated/services/tools.py Moves bucket from resource_id to project_id for create.
python/src/basecamp/generated/services/card_columns.py Emits project_id=bucket_id for card-column ops.
python/scripts/generate_services.py Generator: treat bucketId as project scope; exclude from resource selection.
kotlin/sdk/src/commonTest/kotlin/com/basecamp/sdk/WormholesServiceTest.kt Adds test asserting bucket→projectId and cardTable→resourceId.
kotlin/sdk/src/commonTest/kotlin/com/basecamp/sdk/WebhooksServiceTest.kt Adds test asserting bucket→projectId and null resourceId.
kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/generated/services/wormholes.kt Emits projectId = bucketId for wormhole ops.
kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/generated/services/webhooks.kt Moves bucket from resourceId to projectId; clears resourceId.
kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/generated/services/tools.kt Moves bucket from resourceId to projectId; clears resourceId.
kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/generated/services/card-columns.kt Emits projectId = bucketId for card-column ops.
kotlin/generator/src/main/kotlin/com/basecamp/sdk/generator/ServiceEmitter.kt Generator: treat bucketId as project scope; exclude from resource selection.
go/pkg/basecamp/observability.go Adds exported OperationInfo.ProjectID.
go/pkg/basecamp/otel/otel.go Emits basecamp.project_id span attribute when present.
go/pkg/basecamp/otel/otel_test.go Updates tracing test expectations to include basecamp.project_id.
go/pkg/basecamp/operation_scope_test.go Adds table-driven regression test pinning {ProjectID, ResourceID} across ops.
go/pkg/basecamp/wormholes.go Sets ProjectID for bucket-scoped wormhole ops.
go/pkg/basecamp/webhooks.go Sets ProjectID for bucket-scoped webhook ops.
go/pkg/basecamp/tools.go Moves bucket from ResourceID to ProjectID for Tools.Create.
go/pkg/basecamp/tools_test.go Updates test to assert {ProjectID=bucket, ResourceID=0}.
go/pkg/basecamp/projects.go Moves project id from ResourceID to ProjectID for Projects.Get/Update/Trash.
go/pkg/basecamp/gauges.go Sets ProjectID for project-scoped gauge ops.
go/pkg/basecamp/people.go Sets ProjectID for project-scoped people ops.
go/pkg/basecamp/timesheet.go Sets ProjectID for project-scoped timesheet report op.
go/pkg/basecamp/timeline.go Sets ProjectID for project-scoped timeline op.
go/pkg/basecamp/cards.go Sets ProjectID for bucket-scoped card-column ops.

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

@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.

All reported issues were addressed across 50 files

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

Re-trigger cubic

Comment thread swift/Tests/BasecampTests/GeneratedServiceTests.swift
Comment thread ruby/test/basecamp/services/webhooks_service_test.rb
Comment thread ruby/test/basecamp/services/webhooks_service_test.rb Outdated
jeremy added 2 commits July 24, 2026 23:52
If on_operation_start never fires, events.find returns nil and nil[:info]
raises an opaque NoMethodError. Assert the event was found first so a wiring
regression points straight at the missing hook. Applied to both the webhooks
and wormholes metadata tests. (review: cubic-dev-ai)
make swift-generate adds DeleteWormhole and UpdateWormhole to the idempotent
operation list — pre-existing drift left by #402 (the committed Metadata.swift
predates those ops landing in the idempotent set), unrelated to project scope
and uncaught because there is no Swift drift gate. Absorb it here so the branch
is reproducible: make generate now leaves the tree clean rather than producing
this diff.
Copilot AI review requested due to automatic review settings July 25, 2026 06:59
@jeremy jeremy added bug Something isn't working python Pull requests that update the Python SDK and removed enhancement New feature or request labels Jul 25, 2026
@github-actions github-actions Bot added documentation Improvements or additions to documentation enhancement New feature or request and removed enhancement New feature or request documentation Improvements or additions to documentation labels Jul 25, 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.

Pull request overview

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

@jeremy
jeremy merged commit 70df79a into main Jul 25, 2026
49 checks passed
@jeremy
jeremy deleted the feat/op-scope-bucketid branch July 25, 2026 07:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working go kotlin python Pull requests that update the Python SDK ruby Pull requests that update the Ruby SDK swift typescript Pull requests that update TypeScript code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Generators: normalize bucketId as project scope in operation metadata (mirror url.go:357)

2 participants