Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
3cc61d7
feat(event-feed): model the account event feed, inbox, and stream tic…
jeremy Sep 16, 2026
ea3c3af
test(event-feed): service tests and cross-language conformance dispatch
jeremy Sep 16, 2026
c84d2a8
docs(event-feed): SPEC §23 wire operations, fixture rosters, and gap …
jeremy Sep 16, 2026
f9dd1e8
test(event-feed): satisfy the per-language lints and pin the new oper…
jeremy Sep 16, 2026
992affb
fix(event-feed): project the nullable feed members, cover performed_b…
jeremy Sep 16, 2026
1b8b43e
docs(spec): restate the per-SDK inventory and idempotency counts for …
jeremy Sep 16, 2026
1c0c73f
docs(migrating): note the PerformedBy field on Go's Event and Webhook…
jeremy Sep 16, 2026
1010baa
fix(event-feed): project Person's nullable members, parse continuatio…
jeremy Sep 16, 2026
201327d
test(conformance): reattach the runner doc comments the feed summariz…
jeremy Sep 16, 2026
9646fd3
test(conformance): cover a recording-level boost with explicit null b…
jeremy Sep 16, 2026
f7205ba
test(fixtures): send the Agent performer's tagline as null and note K…
jeremy Sep 16, 2026
a68935d
fix(webhooks): carry a performer's tagline on WebhookEventPerson
jeremy Sep 16, 2026
42142d9
docs(event-feed): say what marking the ticket sensitive actually does
jeremy Sep 16, 2026
a55fbaf
feat(event-feed): carry FeedEvent.details verbatim as a document
jeremy Sep 16, 2026
f2a39f5
docs(api-gaps): drop the FeedEventDetails reference the document chan…
jeremy Sep 16, 2026
702adc8
Give PollInbox's 403 a bare error shape
jorgemanrubia Sep 16, 2026
274bfd2
Scope BareForbiddenError's claim to the guard that makes it true
jorgemanrubia Sep 16, 2026
bdfb52b
Say the 403 exception in the doc comment that ships
jorgemanrubia Sep 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Paths are from the repository root, since that is where you will be working.
| **Python** | httpx via `HttpClient` | `python/src/basecamp/generated/services/*.py` |
| **Rust** | reqwest via `HttpClient` trait | `rust/basecamp-sdk/src/generated/services/*.rs` |

All `262` operations across the ~50-service per-SDK layer are generated. Hand-written code is limited to infrastructure: <!-- @operation-count -->
All `265` operations across the ~50-service per-SDK layer are generated. Hand-written code is limited to infrastructure: <!-- @operation-count -->
Comment thread
jeremy marked this conversation as resolved.

| Purpose | Location |
|---------|----------|
Expand Down Expand Up @@ -94,7 +94,7 @@ Pull the andon cord when you see:

All new API coverage starts in `spec/basecamp.smithy`. Before writing SDK code, add operations and shapes to the spec.

`spec/basecamp.smithy` holds `262` worked operations. <!-- @operation-count --> Copy the nearest one rather than
`spec/basecamp.smithy` holds `265` worked operations. <!-- @operation-count --> Copy the nearest one rather than
working from a skeleton here: it shows the live conventions for naming, `@http` URIs,
pagination traits and shape reuse, and it cannot drift from itself.

Expand Down
52 changes: 52 additions & 0 deletions MIGRATING.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,58 @@ what wrong behaviour you get if you ignore one. This file is that half.

# Unreleased

### Go: `Event` and `WebhookEvent` gain `PerformedBy` (#898)
Comment thread
jeremy marked this conversation as resolved.

**The compile error, if you get one:** an unkeyed composite literal of
`basecamp.Event` or `basecamp.WebhookEvent` no longer compiles — each struct
gained an exported `PerformedBy` field (the agent that carried out a delegated
action; nil for a direct one), inserted after `Creator`, so the old value count
is now too short. Same class of break as `Error.RetryAfter` (#795) and
`Error.FieldErrors` (#541), and the remedy is the same, and permanent: use keyed
fields.

```go
// before
e := basecamp.Event{id, recordingID, "created", 0, "", nil, createdAt, &creator}
// after — and it will not break again
e := basecamp.Event{ID: id, RecordingID: recordingID, Action: "created", CreatedAt: createdAt, Creator: &creator}
```

`apidiff` reports the fields as compatible, and it is right about what it
measures: they are additive to the exported API surface. Unkeyed literals are a
source-compatibility hazard the tool does not model, which is why this note
exists. Keyed literals, and every decode path, are unaffected; the field is
`nil`/absent unless BC3 sent `performed_by`.

**Swift: a Smithy document now renders as `JSONValue?`, not `String?`.** The
generator mapped an untyped document member to `String?`, which cannot decode
the object BC3 sends; `WebhookEvent.details` (and the new `FeedEvent.details`)
are `JSONValue?` now, and `JSONValue` gained `Encodable`. A caller that typed
`event.details` as `String?` needs `if case .object(let details)? = event.details`.
Numbers in a `JSONValue` are `Double`.

**Kotlin has the same hazard in a different spelling.** The generated `Event`
and `WebhookEvent` data classes gain `performedBy: Person? = null` inserted
before `boostsCount`/`boostsUrl` and before `copy` respectively, so a positional
constructor call with values in the old order, and a positional destructuring
(`val (id, recordingId, …) = event`) that reaches past `creator`, shift by one
component. Use named arguments and property access; the decoders are unaffected.

**TypeScript and Python: `Person`'s `email_address`, `title`, `bio`, `tagline`
and `location` are typed nullable.** BC3's person partial always writes the five
keys and writes `null` when there is no value — routinely so for an `Agent`
performer, whose `email_address` and `title` are null — so the generated types
now say `string | null` / `str | None` where they said `string` / `str`. Code
that narrowed on `!== undefined` alone needs `!= null`; Go, Kotlin, Swift and
Rust already rendered these as optional-nullable and are unchanged.

The same change adds the account event feed's wire layer (`PollEvents`,
`PollInbox`, `CreateStreamTicket` on the `EventFeed` tag, service `eventFeed`)
to every SDK. Those are new operations and new generated interface methods —
additive for every caller, breaking only for Go code that implements the
generated `ClientInterface` / `ClientWithResponsesInterface` itself, which the
generated client is the only intended implementer of.

### Python: a malformed list body is now an `ApiError`, and two of those changes are silent

A list response the SDK could not read used to leave the Python SDK in one of
Expand Down
8 changes: 4 additions & 4 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,14 +203,14 @@ tracing::info!(?safe, "response headers");
## Retry Behavior

Retry eligibility is decided per *operation*, not per HTTP method. `behavior-model.json` classifies
all `262` operations: the 128 GETs are retryable by method, and 91 mutations are flagged <!-- @operation-count -->
`idempotent: true` — all 53 PUTs, all 27 DELETEs, and 11 POSTs (`CompleteTodo`, `PauseQuestion`,
all `265` operations: the 130 GETs are retryable by method, and 92 mutations are flagged <!-- @operation-count -->
`idempotent: true` — all 53 PUTs, all 27 DELETEs, and 12 POSTs (`CompleteTodo`, `PauseQuestion`,
`SubscribeToCardColumn`, `Subscribe`, `EnableCardColumnOnHold`, `CreateBookmark`, `PrioritizeAssignment`,
`SpotlightRecording`, `RecordProjectVisit`, `CreateBubbleUp`, `EnableProjectClients`). The other 43 POSTs are attempted exactly once. SPEC.md §7 specifies the
`SpotlightRecording`, `RecordProjectVisit`, `CreateBubbleUp`, `EnableProjectClients`, `CreateStreamTicket`). The other 43 POSTs are attempted exactly once. SPEC.md §7 specifies the
Comment thread
jeremy marked this conversation as resolved.
three-gate algorithm and the per-SDK divergences.

- **Reads (GET)**: retried with exponential backoff on 429/503 in every SDK. (HEAD is idempotent by method too, but Ruby's transport gates on `method == :get` specifically, so a HEAD would not retry there. The API surface has no HEAD operations today, so this is theoretical.)
- **Naturally-idempotent mutations (PUT/DELETE) and the 11 flagged POSTs**: *are* retried on 429/503
- **Naturally-idempotent mutations (PUT/DELETE) and the 12 flagged POSTs**: *are* retried on 429/503
by Go (generated operation path), Python, TypeScript, Kotlin, Swift, and Rust. Retrying these cannot
duplicate a resource, which is why the gate is idempotency rather than "is it a mutation".
One PUT narrows its own set: `UpdateProjectClientAccess` declares `retry_on: [503]`, because its
Expand Down
Loading