Skip to content

feat(bubble-ups): wrap bubbling a recording up and down - #839

Merged
jeremy merged 2 commits into
mainfrom
feat/recording-bubble-up
Sep 2, 2026
Merged

feat(bubble-ups): wrap bubbling a recording up and down#839
jeremy merged 2 commits into
mainfrom
feat/recording-bubble-up

Conversation

@jeremy

@jeremy jeremy commented Sep 2, 2026

Copy link
Copy Markdown
Member

What

Adds the per-recording write side of Bubble Up (the BC5 successor to
"save") to all six SDKs. Until now only the read side existed — GetBubbleUps
models the account-user /my/readings/bubble_ups.json list; there was no way to
bubble a single recording up or pop it back down.

Two operations, both answering 204 No Content with no body from
Recordings::BubbleUpsController:

  • CreateBubbleUpPOST /recordings/{recordingId}/bubble_up.json — with
    an optional at timing field.
  • DeleteBubbleUpDELETE /recordings/{recordingId}/bubble_up.json.

Both are naturally idempotent (bubbling an already-bubbled recording, or popping
an absent one, is a set-membership no-op that still answers 204). This mirrors
the DeleteBookmark precedent, which ships off the identical 204 shape, and
the PinMessage/UnpinMessage pair (POST/DELETE 204, empty output).

New BubbleUps tag → BubbleUpsService across all six SDKs via the default
tag→service fallback (zero generator overrides). Go gets the hand-written
wrapper + AccountClient.BubbleUps() accessor; TS/Ruby/Python get their
hand-written client accessors; Swift/Kotlin accessors are generated.

The at field — a bc3 behavior finding

at controls timing. "now" bubbles up immediately; a scheduling keyword
("today", "tomorrow", "weekend", "next_week") or an ISO8601 date
(e.g. "2026-09-10") schedules it to resurface later.

bc3 currently requires at. Reading::Bubbleupable#bubble_up routes
"now" to bubble_up_now, but every other value — including a nil from an
omitted param — flows to Reading::BubbleUpSchedule#bubble_up_at=, whose else
branch calls Date.iso8601(value), and Date.iso8601(nil) raises. So the
operator-verified "optional, now-vs-scheduled" contract holds for scheduling but
omitting at errors server-side today — callers must send "now" for the
immediate case. The field is still modeled optional (not @required) so a
one-line bc3 default (params[:at] ||= "now") would make omission mean "now"
with no SDK change. Flagged as a suggested bc3 follow-up in the gap entry.

Route spelling

Modeled at the canonical flat /recordings/{id}/bubble_up.json, consistent
with the whole recording_actions family the SDK already ships flat (bookmark,
spotlight, pin, position). resource :bubble_up (bc3 config/routes.rb:232)
sits in concern :recording_actions, included flat at the CANONICAL
resources :recordings (:276) and separately bucket-scoped at :918. The
sibling resource :position in the same concern has a test/api test proving
the concern's flat routes answer on the API host.

bc3's doc/api documents neither spelling yet, so the two routes are waived
in spec/bc3-route-allowlist.yml (sdk_routes_absent_from_bc3_docs) with
routes.rb + controller evidence. Documenting the create/destroy contract in
bc3 doc/api would let both waivers drop.

Remaining API gap — per-recording status GET

GET /recordings/{id}/bubble_up.json (the show action) is not absorbable
and stays a gap. Recordings::BubbleUpsController#show renders
app/views/recordings/bubble_ups/show.json.jbuilder, which lives outside
app/views/api/; restrict_view_paths_to_api_root limits the API host to that
root, so the template is unrenderable there — the same trap that condemned
GetRecording. The head :no_content create/destroy sidestep it (no template).
Tracked as partial-coverage in
spec/api-gaps/recording-bubble-up-write.md, with the bc3 fix noted (add an
app/views/api/recordings/bubble_ups/show.json.jbuilder, then a GetBubbleUp
op + BubbleUpStatus shape close it).

Tests & checks

  • Conformance (all six runners): paths.json — create-with-at,
    create-omitting-at (asserts the SDK leaves it off the wire), delete;
    idempotency.json — both retry on 503 (Ruby GET-only-retry skips recorded in
    zero-skip-roster.yml).
  • Unit tests: Go wrapper (create at present/absent, delete, 4xx) + Ruby /
    Python / TypeScript service tests.
  • Green: smithy build, six-language make generate, all parity gates
    (idempotency, write-semantics, retry-metadata, service-inventory,
    operation-assignment, bucket-flat, grouped-client, bc3-route-parity,
    validate-api-gaps, doc-constants), Go/TS/Ruby/Python/Kotlin drift + unit +
    conformance, Swift drift + SDK/runner build.
  • Not run locally: Swift XCTest execution (needs full Xcode; the Command
    Line Tools lack XCTest — SDK and conformance runner both compile clean) and
    lint-actions (a local zizmor version flags pre-existing .github/workflows
    uses: ./… findings identically on origin/main; no workflow files changed).
    Both pass in CI.

Release note: Features (enhancement).

Tracks Basecamp card 10264451804.


Summary by cubic

Adds the per-recording write side of Bubble Up to all six SDKs: CreateBubbleUp (POST) and DeleteBubbleUp (DELETE) at /recordings/{recordingId}/bubble_up.json. Previously only the read side existed (GetBubbleUps); now a recording can be bubbled up and popped back down.

Both operations return 204 with no body and are naturally idempotent, so retrying on 503 is safe. CreateBubbleUp accepts an optional at timing field; bc3 currently rejects an omitted at server-side, so send "now" for immediate bubble-up. The field is modeled optional so a future bc3 default could make omission mean "now" without SDK changes.

New Features

  • Adds a BubbleUps service to Go, TypeScript, Ruby, Python, Swift, and Kotlin, with Python async coverage.
  • Conformance now asserts at reaches the wire when provided and is absent when omitted; adds idempotency coverage and unit tests for create (with and without at) and delete.

Remaining gap

  • GET /recordings/{recordingId}/bubble_up.json stays uncovered because bc3's show template is outside the API view path; tracked as partial coverage.
  • The two write routes are allowlisted for bc3 route parity because bc3's doc/api doesn't document them yet.

Written for commit e038655. Summary will update on new commits.

Review in cubic

Adds CreateBubbleUp and DeleteBubbleUp — the per-recording write side of
Bubble Up (the BC5 successor to "save"), which had no SDK coverage:
GetBubbleUps only models the /my/readings list.

- CreateBubbleUp: POST /recordings/{id}/bubble_up.json (204), optional `at`
  timing field ("now" immediate; a keyword or ISO8601 date schedules).
- DeleteBubbleUp: DELETE /recordings/{id}/bubble_up.json (204).

Both answer head :no_content from Recordings::BubbleUpsController and are
naturally idempotent (set membership), mirroring the DeleteBookmark shape.
New BubbleUps tag/service across all six SDKs (Go wrapper + accessor;
TS/Ruby/Python client accessors; Swift/Kotlin generated). Conformance path
+ idempotency cases dispatched in all six runners; Go/TS/Ruby/Python unit
tests covering create (at present/absent) and delete.

The per-recording status GET stays an API gap: its show.json.jbuilder lives
outside app/views/api/, unrenderable on the API host (the GetRecording trap).
Registered in spec/api-gaps/recording-bubble-up-write.md (partial-coverage).
The two flat routes are waived in bc3-route-allowlist.yml with routes.rb +
controller evidence, since bc3's doc/api documents neither spelling yet.
Copilot AI balanced review requested due to automatic review settings September 2, 2026 16:52
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 2, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-02T17:35:23.292089Z e038655 Manual request
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions github-actions Bot added typescript Pull requests that update TypeScript code ruby Pull requests that update the Ruby SDK go kotlin swift spec Changes to the Smithy spec or OpenAPI conformance Conformance test suite python Pull requests that update the Python SDK labels Sep 2, 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.

🟡 Changes recommended

Conformance payload assertions, asynchronous Python coverage, and several documentation counts remain incomplete.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds Smithy-first Bubble Up create/delete support across all six SDKs, including generated services, client accessors, retry metadata, and conformance coverage.

Changes:

  • Adds idempotent CreateBubbleUp and DeleteBubbleUp operations.
  • Regenerates six SDK surfaces and wires service accessors.
  • Adds tests, route waivers, gap tracking, and documentation updates.

[!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.

File summaries
File Description
typescript/tests/services/bubble-ups.test.ts Tests TypeScript operations.
typescript/src/index.ts Exports Bubble Ups APIs.
typescript/src/generated/services/index.ts Exports generated service.
typescript/src/generated/services/bubble-ups.ts Implements generated service.
typescript/src/generated/schema.d.ts Adds generated operation types.
typescript/src/generated/path-mapping.ts Maps Bubble Up routes.
typescript/src/generated/openapi-stripped.json Adds stripped OpenAPI operations.
typescript/src/generated/metadata.ts Adds retry metadata.
typescript/src/client.ts Wires the service accessor.
swift/Sources/Basecamp/Generated/Services/BubbleUpsService.swift Implements Swift service.
swift/Sources/Basecamp/Generated/Models/CreateBubbleUpRequest.swift Adds Swift request model.
swift/Sources/Basecamp/Generated/Metadata.swift Adds Swift retry metadata.
swift/Sources/Basecamp/Generated/AccountClient+Services.swift Adds Swift accessor.
swift/README.md Updates service counts.
spec/zero-skip-roster.yml Records Ruby retry waivers.
spec/overlays/tags.smithy Assigns the BubbleUps tag.
spec/bc3-route-allowlist.yml Waives undocumented BC3 routes.
spec/basecamp.smithy Defines both operations.
spec/api-gaps/recording-bubble-up-write.md Tracks partial API coverage.
spec/api-gaps/README.md Registers the API gap.
SPEC.md Updates inventories and counts.
SECURITY.md Updates retry classification.
scripts/check-idempotency-parity Updates expected idempotency totals.
ruby/test/basecamp/services/bubble_ups_service_test.rb Tests Ruby service behavior.
ruby/lib/basecamp/generated/types.rb Refreshes generated types metadata.
ruby/lib/basecamp/generated/services/bubble_ups_service.rb Implements Ruby service.
ruby/lib/basecamp/generated/metadata.json Adds Ruby retry metadata.
ruby/lib/basecamp/client.rb Adds Ruby accessor.
python/tests/services/test_bubble_ups_service.py Tests synchronous Python service.
python/src/basecamp/generated/types.py Adds Python request type.
python/src/basecamp/generated/services/bubble_ups.py Implements sync/async services.
python/src/basecamp/generated/services/__init__.py Exports generated services.
python/src/basecamp/generated/metadata.json Adds Python retry metadata.
python/src/basecamp/client.py Adds synchronous accessor.
python/src/basecamp/async_client.py Adds asynchronous accessor.
openapi.json Adds canonical API definitions.
kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/generated/services/Types.kt Adds Kotlin request body.
kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/generated/services/bubble-ups.kt Implements Kotlin service.
kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/generated/ServiceAccessors.kt Adds Kotlin accessor.
kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/generated/Metadata.kt Adds Kotlin retry metadata.
kotlin/README.md Updates service counts.
kotlin/conformance/src/main/kotlin/com/basecamp/sdk/conformance/Main.kt Dispatches Kotlin conformance cases.
go/pkg/generated/client.gen.go Adds generated Go operations.
go/pkg/basecamp/url-routes.json Registers Go route metadata.
go/pkg/basecamp/client.go Adds Go service accessor.
go/pkg/basecamp/bubble_ups.go Implements Go wrapper.
go/pkg/basecamp/bubble_ups_test.go Tests Go wrapper behavior.
go/grouped-client-inventory.yml Accounts for new Go operations.
conformance/tests/paths.json Adds route and payload cases.
conformance/tests/idempotency.json Adds retry cases.
conformance/runner/typescript/runner.test.ts Dispatches TypeScript cases.
conformance/runner/swift/Sources/ConformanceRunner/Dispatch.swift Dispatches Swift cases.
conformance/runner/ruby/runner.rb Dispatches Ruby cases and waivers.
conformance/runner/python/runner.py Dispatches Python cases.
conformance/runner/go/main.go Dispatches Go cases.
behavior-model.json Classifies retry behavior.
AGENTS.md Updates operation totals.
Review details

Suppressed comments (1)

conformance/tests/paths.json:1329

  • Despite this case's name and description, its assertions never inspect the request body, so an SDK that serializes at: null would pass. Add requestBodyAbsent to make the advertised six-runner omission check effective.
    "name": "CreateBubbleUp omits 'at' from the wire when not supplied",
    "description": "Verifies CreateBubbleUp POSTs /recordings/{recordingId}/bubble_up.json and leaves 'at' off the body when the caller supplies none (the SDK does not force a value). Accepts 204.",
  • Files reviewed: 35/57 changed files
  • Comments generated: 5
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread conformance/tests/paths.json
Comment thread python/tests/services/test_bubble_ups_service.py
Comment thread SPEC.md Outdated
Comment thread SPEC.md
Comment thread spec/api-gaps/recording-bubble-up-write.md Outdated

@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: 4b2a7cddbd

ℹ️ 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 spec/basecamp.smithy
Comment thread spec/overlays/tags.smithy

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

8 issues found across 57 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="python/tests/services/test_bubble_ups_service.py">

<violation number="1" location="python/tests/services/test_bubble_ups_service.py:17">
P3: This new test file covers only the sync BubbleUpsService, but the PR also adds AsyncBubbleUpsService with the same create_bubble_up/delete_bubble_up methods. Sibling service tests (e.g. test_campfires_service.py, test_cards.py) exercise the async client too. Add async equivalents (via AsyncClient(...).for_account(...).bubble_ups) so the generated async surface stays covered.</violation>
</file>

<file name="SECURITY.md">

<violation number="1" location="SECURITY.md:188">
P3: The new count of 10 idempotent POSTs on line 188 leaves the unchanged reference on line 194 ('the 9 flagged POSTs') stale, so the same document now contradicts itself. Update line 194 to 'the 10 flagged POSTs' to match the new count.</violation>
</file>

<file name="conformance/tests/paths.json">

<violation number="1" location="conformance/tests/paths.json:1298">
P2: This test sends `at` in requestBody and its description says it verifies the optional `at` schedule, but the assertions never check that `at` reached the wire. A SDK that drops the field (or serializes it wrong) would pass. Add a `requestBody` assertion with `path: "at"` and `expected: "2026-09-10T09:00:00Z"` to pin the serialization.</violation>

<violation number="2" location="conformance/tests/paths.json:1329">
P2: This test exists solely to pin that the SDK omits `at` from the wire when the caller supplies none, but its assertions (requestPath + noError) never inspect the request body. An SDK that forces `at` onto the body would still pass, so the claimed behavior is unverified. Add a `requestBodyAbsent` assertion with `path: "at"` so the six-SDK omission is actually enforced.</violation>
</file>

<file name="SPEC.md">

<violation number="1" location="SPEC.md:4221">
P3: Appendix E's breakdown no longer matches its own total or behavior-model.json. The total was bumped to 256 but 'Idempotent: 86' still reflects the old 254-operation count; the two new idempotent operations (CreateBubbleUp, DeleteBubbleUp) make it 88, so 86+168=254 contradicts the 256 just stated above. Update the idempotent count to 88.</violation>

<violation number="2" location="SPEC.md:4393">
P3: The per-SDK Service Coverage table is now internally inconsistent. bubbleUps is wired into all six SDKs, but only the Swift and Kotlin rows were bumped to 54; Ruby, TypeScript, and Python still claim 53 'full canonical set', and Go still reads '51 accessors ... Capability is 53/53' (now 52 accessors, capability 54/54). Update the four stale rows to match.</violation>
</file>

<file name="spec/bc3-route-allowlist.yml">

<violation number="1" location="spec/bc3-route-allowlist.yml:130">
P3: The new entries cite line numbers for the same concern/block that contradict the existing position entries. The new POST/DELETE entries say concern :recording_actions (:206) is 'included flat at :276 inside `resources :recordings, only: []` (:261...)' and 'bucket-scoped at :918', while the position entries in the same file describe the same include site as :273/:258/:902. A concern include is a single line, so it can't be both. Reconcile the numbers against the pinned bc3 revision (update whichever set is stale) so the evidence a reader checks is internally consistent.</violation>
</file>

<file name="spec/api-gaps/recording-bubble-up-write.md">

<violation number="1" location="spec/api-gaps/recording-bubble-up-write.md:111">
P3: Rename this case summary to `create-without-`at``; the corresponding `paths.json` fixture omits `at` rather than sending `"now"`.</violation>
</file>

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

Re-trigger cubic

Comment thread conformance/tests/paths.json
Comment thread conformance/tests/paths.json
Comment thread python/tests/services/test_bubble_ups_service.py
Comment thread SECURITY.md
Comment thread SPEC.md
Comment thread SPEC.md
Comment thread spec/bc3-route-allowlist.yml
Comment thread spec/api-gaps/recording-bubble-up-write.md Outdated
Conformance: assert `at` reaches the wire on the scheduled create case
and is absent on the omit case (requestBody / requestBodyAbsent), so a
runner that drops or forces the field can no longer pass vacuously.

Python: add async create/delete coverage (AsyncClient.bubble_ups) mirroring
the sync tests, matching sibling recording-mutation service tests.

Docs: idempotent operation count 86 -> 88 (SPEC.md Appendix E); per-SDK
service coverage 53 -> 54 for Ruby/TypeScript/Python and 51 -> 52 accessors
(54/54 capability) for Go; SECURITY.md "9 flagged POSTs" -> 10; api-gap
case name create-now -> create-without-`at` to match the fixture.

Route allowlist: reconcile the recording_actions concern-include line
numbers (flat :276 / recordings-only-[] :261 / bucket :918 / sibling
door :963) against the current bc3 pin 88549ca6; the pre-existing position
entries still cited the prior pin, contradicting the correct bubble-up
entries.
@jeremy jeremy added the breaking Breaking change to public API label Sep 2, 2026
@jeremy

jeremy commented Sep 2, 2026

Copy link
Copy Markdown
Member Author

@codex 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: e03865568c

ℹ️ 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 spec/basecamp.smithy
@jeremy
jeremy merged commit f439f49 into main Sep 2, 2026
59 of 61 checks passed
@jeremy
jeremy deleted the feat/recording-bubble-up branch September 2, 2026 20:59
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 conformance Conformance test suite go kotlin python Pull requests that update the Python SDK ruby Pull requests that update the Ruby SDK spec Changes to the Smithy spec or OpenAPI swift typescript Pull requests that update TypeScript code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants