Normalize bucketId as project scope in operation metadata (#422)#440
Conversation
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.
There was a problem hiding this comment.
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
bucketIdas project scope (projectId/project_id) and exclude it fromresourceIdselection. - Generated services: populate
projectIdfor the ninebucketId-scoped operations; clearresourceIdfor bucket-scoped collection operations (ListWebhooks,CreateWebhook,CreateTool). - Go observability: add
OperationInfo.ProjectID, emitbasecamp.project_idotel 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.
There was a problem hiding this comment.
All reported issues were addressed across 50 files
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
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.
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, sobucketId-labeled operations (/{accountId}/buckets/{bucketId}/…) emittedprojectId = null, and on the threeaccountId+bucketId-only paths the bucket id was additionally mis-emitted asresourceId. This normalizesbucketIdas 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 incheck-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.ProjectIDfield (Go previously had no project field at all).What changed
Nine
bucketId-labeled operations —SetCardColumnColor,EnableCardColumnOnHold,DisableCardColumnOnHold,CreateWormhole,UpdateWormhole,DeleteWormhole,ListWebhooks,CreateWebhook,CreateTool:projectId(= the bucket id).ListWebhooks,CreateWebhook,CreateTool) — theaccountId+bucketId-only paths — changeresourceIdfrom the bucket id to null. The other six keep their deeper id (columnId/cardTableId/wormholeId).Generators (one commit per SDK): match
projectIdorbucketIdfor project scope and emit the matched param's actual local name; excludebucketId(as well asprojectId) fromresourceIdselection.Go hand-written layer: add exported
OperationInfo.ProjectID, emit abasecamp.project_idotel span attribute, and populateProjectID/ResourceIDat 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 ofResourceIDforTools.CreateandProjects.Get/Update/Trash, matching the generated SDKs.Contract vs. API framing
spec/api-provenance.jsonbump.openapi.json,behavior-model.json, andurl-routes.jsonregenerate byte-identical. Operation and service counts are unchanged (208 ops).Release notes / compatibility
OperationInfogains an exportedProjectID int64. Operations with a project/bucket path label now populate it, and the otel hook emits abasecamp.project_idspan attribute alongsidebasecamp.resource_id.resourceId/ResourceIDsemantics tightened for the three collection ops (ListWebhooks,CreateWebhook,CreateTool) and, in Go, forTools.CreateandProjects.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 offresourceIdfor these specific ops will see the value move toprojectId.golang.org/x/exp/apidiff. AddingOperationInfo.ProjectIDis source-compatible except for unkeyed struct literals ofOperationInfo— a case apidiff ignores by design.OperationInfois 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
{ProjectID, ResourceID}split for every project/bucket-scoped op — mechanically guaranteeing no forgotten assignment. Existingtools_test.goandotel_test.goassertions updated for the new split.CreateWormhole(bucket becomes project scope, deeper resource id kept) andCreateTool/ListWebhooks(bucket becomes project scope, resource id null).Reproducibility note
One commit absorbs a pre-existing
swift/Sources/Basecamp/Generated/Metadata.swiftregeneration artifact (addsDeleteWormhole/UpdateWormholeto 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 generatenow leaves the tree clean (modulo generation timestamps).Verification
make checkandmake url-routes-checkboth green (conformance, drift checks, bucket-flat parity, api-gaps, deprecation parity).url-routes.json,openapi.json,behavior-model.json, and provenance regenerate byte-identical.