From 759b38a6763c71915334efb28725cab82f980a3a Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Wed, 22 Jul 2026 00:11:06 -0700 Subject: [PATCH 01/12] sync: repin provenance to bc3 master ba105ba7 (2026-07-22) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move .bc3 from e52453ff (2026-07-17, one day pre-train) to current master HEAD. Drift across the window is either registry-tracked in spec/api-gaps/ or example-regen catch-up; the modeled-surface fixes and new registry briefs land in the following commits. Propagated via make sync-spec-version + provenance-sync + generate: smithy service version, openapi.json info.version, and all six API_VERSION constants now read 2026-07-22. compatibility.bc3-four stays at 9d73959a — the four branch has zero API drift since (verified 2026-07-22: no doc/api or api controller/view changes). --- go/pkg/basecamp/api-provenance.json | 4 ++-- go/pkg/basecamp/version.go | 2 +- .../src/commonMain/kotlin/com/basecamp/sdk/BasecampConfig.kt | 2 +- openapi.json | 2 +- python/src/basecamp/_version.py | 2 +- ruby/lib/basecamp/version.rb | 2 +- spec/api-provenance.json | 4 ++-- spec/basecamp.smithy | 2 +- swift/Sources/Basecamp/BasecampConfig.swift | 2 +- typescript/src/client.ts | 2 +- typescript/src/generated/openapi-stripped.json | 2 +- 11 files changed, 13 insertions(+), 13 deletions(-) diff --git a/go/pkg/basecamp/api-provenance.json b/go/pkg/basecamp/api-provenance.json index ad2ef0b78..94869882a 100644 --- a/go/pkg/basecamp/api-provenance.json +++ b/go/pkg/basecamp/api-provenance.json @@ -1,8 +1,8 @@ { "bc3": { "branch": "master", - "revision": "e52453ff35a6292473eb095a8568649aa132fef9", - "date": "2026-07-17" + "revision": "ba105ba7d7e48bd97afdc98305e9fb8a63a88beb", + "date": "2026-07-22" }, "compatibility": { "bc3-four": { diff --git a/go/pkg/basecamp/version.go b/go/pkg/basecamp/version.go index ee5c4bcab..799ecccfb 100644 --- a/go/pkg/basecamp/version.go +++ b/go/pkg/basecamp/version.go @@ -4,4 +4,4 @@ package basecamp const Version = "0.8.0" // APIVersion is the Basecamp API version this SDK targets. -const APIVersion = "2026-07-17" +const APIVersion = "2026-07-22" diff --git a/kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/BasecampConfig.kt b/kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/BasecampConfig.kt index dde0b8db4..6fcf9f468 100644 --- a/kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/BasecampConfig.kt +++ b/kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/BasecampConfig.kt @@ -29,7 +29,7 @@ data class BasecampConfig( ) { companion object { const val VERSION = "0.8.0" - const val API_VERSION = "2026-07-17" + const val API_VERSION = "2026-07-22" const val DEFAULT_BASE_URL = "https://3.basecampapi.com" const val DEFAULT_USER_AGENT = "basecamp-sdk-kotlin/$VERSION (api:$API_VERSION)" const val DEFAULT_MAX_RETRIES = 3 diff --git a/openapi.json b/openapi.json index 44ca048eb..29ab71d06 100644 --- a/openapi.json +++ b/openapi.json @@ -2,7 +2,7 @@ "openapi": "3.1.0", "info": { "title": "Basecamp", - "version": "2026-07-17", + "version": "2026-07-22", "description": "Basecamp API", "contact": { "name": "Basecamp", diff --git a/python/src/basecamp/_version.py b/python/src/basecamp/_version.py index ef313c6a3..4f73de5b9 100644 --- a/python/src/basecamp/_version.py +++ b/python/src/basecamp/_version.py @@ -1,2 +1,2 @@ VERSION = "0.8.0" -API_VERSION = "2026-07-17" +API_VERSION = "2026-07-22" diff --git a/ruby/lib/basecamp/version.rb b/ruby/lib/basecamp/version.rb index 6e069f88c..20b30a59e 100644 --- a/ruby/lib/basecamp/version.rb +++ b/ruby/lib/basecamp/version.rb @@ -2,5 +2,5 @@ module Basecamp VERSION = "0.8.0" - API_VERSION = "2026-07-17" + API_VERSION = "2026-07-22" end diff --git a/spec/api-provenance.json b/spec/api-provenance.json index ad2ef0b78..94869882a 100644 --- a/spec/api-provenance.json +++ b/spec/api-provenance.json @@ -1,8 +1,8 @@ { "bc3": { "branch": "master", - "revision": "e52453ff35a6292473eb095a8568649aa132fef9", - "date": "2026-07-17" + "revision": "ba105ba7d7e48bd97afdc98305e9fb8a63a88beb", + "date": "2026-07-22" }, "compatibility": { "bc3-four": { diff --git a/spec/basecamp.smithy b/spec/basecamp.smithy index 4f6b03451..01db7659b 100644 --- a/spec/basecamp.smithy +++ b/spec/basecamp.smithy @@ -51,7 +51,7 @@ use basecamp.traits#basecampAuthRoutableUrl /// Basecamp API @restJson1 service Basecamp { - version: "2026-07-17" + version: "2026-07-22" rename: { "smithy.api#Document": "JsonDocument" } diff --git a/swift/Sources/Basecamp/BasecampConfig.swift b/swift/Sources/Basecamp/BasecampConfig.swift index b977e5b31..c6d76c5d6 100644 --- a/swift/Sources/Basecamp/BasecampConfig.swift +++ b/swift/Sources/Basecamp/BasecampConfig.swift @@ -27,7 +27,7 @@ public struct BasecampConfig: Sendable { public static let version = "0.8.0" /// Basecamp API version this SDK targets. - public static let apiVersion = "2026-07-17" + public static let apiVersion = "2026-07-22" /// Default User-Agent header value. public static let defaultUserAgent = "basecamp-sdk-swift/\(version) (api:\(apiVersion))" diff --git a/typescript/src/client.ts b/typescript/src/client.ts index c7b51b22d..7105dd888 100644 --- a/typescript/src/client.ts +++ b/typescript/src/client.ts @@ -220,7 +220,7 @@ export interface BasecampClientOptions { } export const VERSION = "0.8.0"; -export const API_VERSION = "2026-07-17"; +export const API_VERSION = "2026-07-22"; const DEFAULT_USER_AGENT = `basecamp-sdk-ts/${VERSION} (api:${API_VERSION})`; /** diff --git a/typescript/src/generated/openapi-stripped.json b/typescript/src/generated/openapi-stripped.json index 28a0e1ab2..008a2c714 100644 --- a/typescript/src/generated/openapi-stripped.json +++ b/typescript/src/generated/openapi-stripped.json @@ -2,7 +2,7 @@ "openapi": "3.1.0", "info": { "title": "Basecamp", - "version": "2026-07-17", + "version": "2026-07-22", "description": "Basecamp API", "contact": { "name": "Basecamp", From 9d4021804dba581dfeb3eb699c798eb22fe80eee Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Wed, 22 Jul 2026 00:13:37 -0700 Subject: [PATCH 02/12] api-gaps: register the three post-train contracts from bc3 #12359 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New briefs (status addressed-in-bc3-pr-12359, merged 2026-07-22): - campfire-line-edit: PUT chats/:c/lines/:id.json, required content, rich-text conversion, creator-only 403/204; SDK PR #295 is the likely absorbing PR. Also records the delete tightening (creator-or-admin). - todoset-direct-todo-create: POST buckets/:b/todosets/:id/todos.json, 201 Todo, project-scoped only; PR-2 CreateTodosetTodo is the vehicle. - schedule-recurrence-writes: full recurrence_schedule param table incl. the week_instance-0 server-hang warning (bc3 #12362 still open), silent-discard-on-invalid, derived response fields, and the already-recurring update redirect; PR-5 vehicle, wire-echo probe gate. search-filter-additions: docs merged — body now lists the documented params (type_names[]/bucket_ids[]/creator_ids[]/since/sort + retained singulars); hold stays, bc3 #12361 is still open. --- spec/api-gaps/README.md | 6 +- spec/api-gaps/campfire-line-edit.md | 75 ++++++++++++++++ spec/api-gaps/schedule-recurrence-writes.md | 96 +++++++++++++++++++++ spec/api-gaps/search-filter-additions.md | 32 ++++--- spec/api-gaps/todoset-direct-todo-create.md | 66 ++++++++++++++ 5 files changed, 263 insertions(+), 12 deletions(-) create mode 100644 spec/api-gaps/campfire-line-edit.md create mode 100644 spec/api-gaps/schedule-recurrence-writes.md create mode 100644 spec/api-gaps/todoset-direct-todo-create.md diff --git a/spec/api-gaps/README.md b/spec/api-gaps/README.md index ad8ff9dab..73a428374 100644 --- a/spec/api-gaps/README.md +++ b/spec/api-gaps/README.md @@ -48,10 +48,14 @@ making the absorption journey publicly auditable. | [recording-bubbleupable-field](recording-bubbleupable-field.md) | no-json-contract | 3e | low | | [todoset-completed-list-visibility](todoset-completed-list-visibility.md) | ambiguous | 3a | low | | [memories-emptied-regression](memories-emptied-regression.md) | addressed-in-bc3-pr-11628 | launch | high | +| [campfire-line-edit](campfire-line-edit.md) | addressed-in-bc3-pr-12359 | post-train | medium | +| [todoset-direct-todo-create](todoset-direct-todo-create.md) | addressed-in-bc3-pr-12359 | post-train | medium | +| [schedule-recurrence-writes](schedule-recurrence-writes.md) | addressed-in-bc3-pr-12359 | post-train | medium | > Statuses reflect how BC3's **BC5 API train** actually shipped (8 PRs merged > to `master`, 2026-07-18..21); BC3 #10947 closed unmerged, superseded by the -> train. `memories-emptied-regression` is a *subtractive* delta (a field BC4 +> train. Entries with plan phase `post-train` track contracts documented +> after the train (BC3 #12359, merged 2026-07-22). `memories-emptied-regression` is a *subtractive* delta (a field BC4 > populates that BC5 emptied), settled as **permanently empty by documented > contract** — its `addressed-in-bc3-pr-11628` records the PR that codified > the empty-placeholder contract, not a repopulation; see the entry. diff --git a/spec/api-gaps/campfire-line-edit.md b/spec/api-gaps/campfire-line-edit.md new file mode 100644 index 000000000..a97c2ff81 --- /dev/null +++ b/spec/api-gaps/campfire-line-edit.md @@ -0,0 +1,75 @@ +--- +gap: campfire-line-edit +status: addressed-in-bc3-pr-12359 +detected: 2026-07-22 +sdk_demand: medium +bc3_pr: 12359 +bc3_refs: + introduced_in: five + routes: + - "PUT /:account_id/chats/:chat_id/lines/:line_id.json" + - "PUT /:account_id/buckets/:project_id/chats/:chat_id/lines/:line_id.json (legacy project-scoped)" + controllers: + - app/controllers/chats/lines_controller.rb + related_existing_api: + - GetCampfireLine + - CreateCampfireLine + - DeleteCampfireLine +--- + +# Campfire line edit + +## What's missing + +SDK absorption only — the contract shipped via BC3 **#12359** (merged +2026-07-22, post-train follow-up to the BC5 API docs). "Update a Campfire +line" in `doc/api/sections/campfires.md` on `master` is the contract of +record: + +- `PUT /chats/:chat_id/lines/:line_id.json` (canonical account-scoped form; + the project-scoped `PUT /buckets/:project_id/chats/:chat_id/lines/:line_id.json` + is documented under Legacy project-scoped routes). +- **Required parameter**: `content` — the new body for the line. +- Only text and rich text lines can be edited, and **only by their creator**. +- The new content is treated as rich text: the line becomes a rich text line + even if it was originally plain text, and `content` may include the HTML + tags covered in the Rich text guide. +- Returns `204 No Content` on success, `403 Forbidden` if the current user + isn't allowed to edit the line. + +The same PR also tightened the documented **delete** contract (already +modeled as `DeleteCampfireLine`): lines can be deleted by their creator *or +by an admin*, and `403 Forbidden` is now documented for the disallowed case. +That's a doc-comment refinement on the existing operation, not a new gap. + +## Why it matters + +Line editing is a BC5 capability with no BC4 analog. Without it, an SDK +consumer that posts Campfire lines (bots, bridges, notifiers) can't correct +a message after the fact — the only recourse is delete + repost, which loses +position and reads as churn in the room. + +## Suggested API shape + +`UpdateCampfireLine` operation: `PUT /{accountId}/chats/{chatId}/lines/{lineId}.json` +with a required `content` body member, `204` response (no output payload), +and `ForbiddenError` in the error list. Input mirrors `CreateCampfireLine`'s +content member; no response-shape work needed. + +## Implementation notes for BC3 + +Shipped — nothing pending. `chats/lines_controller.rb` serves the route; +the doc documents request/response and the permission model (creator-only +edit; creator-or-admin delete). + +## SDK absorption plan when this lands + +- **SDK PR #295 (open) already models `UpdateCampfireLine`** and is the + likely absorbing PR; the §Q absorption queue's PR-2 build-ahead pair is + the fallback vehicle if #295 stalls. +- Status flips to `absorbed-in-sdk` with the absorbing PR (which adds the + Smithy refs). +- While absorbing, refresh `DeleteCampfireLine`'s doc comment with the + creator-or-admin / 403 language from the same doc section. +- Pairwise check: route 404s on BC4 (no line editing), succeeds on BC5 — + additive-only, no invariant violation. diff --git a/spec/api-gaps/schedule-recurrence-writes.md b/spec/api-gaps/schedule-recurrence-writes.md new file mode 100644 index 000000000..822b16882 --- /dev/null +++ b/spec/api-gaps/schedule-recurrence-writes.md @@ -0,0 +1,96 @@ +--- +gap: schedule-recurrence-writes +status: addressed-in-bc3-pr-12359 +detected: 2026-07-22 +sdk_demand: medium +bc3_pr: 12359 +bc3_refs: + introduced_in: five + routes: + - "POST /:account_id/schedules/:schedule_id/entries.json (existing — recurrence params additive)" + - "PUT /:account_id/schedule_entries/:id.json (existing — recurrence params additive)" + controllers: + - app/controllers/schedules/entries_controller.rb + related_existing_api: + - CreateScheduleEntry + - UpdateScheduleEntry + - GetScheduleEntry +--- + +# Schedule entries — recurrence write parameters + +## What's missing + +Write-side recurrence for schedule entries. The read surface +(`recurrence_schedule` in Get a schedule entry) has long been documented; +BC3 **#12359** (merged 2026-07-22, post-train follow-up; cURL example +aligned by #12363) documents the **write** contract in +`doc/api/sections/schedule_entries.md` on `master`, as additive parameters +on the existing create/update endpoints: + +- `recurrence_schedule` — object that makes the entry recurring: + + | Param | Applies to | Contract | + |---|---|---| + | `frequency` | required in the object | one of `every_day`, `every_weekday`, `every_week`, `every_other_week`, `every_month`, `every_day_of_month`, `every_year`, `custom_week`, `custom_month` | + | `days` | `every_day`; `custom_month` without `week_instance` | for `every_day`: days of week as integers `0` (Sunday)–`6` (Saturday), omit to recur every day; for `custom_month`: day of month `1`–`31`. Derived from `starts_at` for the other frequencies | + | `week_instance` | `every_month`, `custom_month` | which week of the month, `1`–`4`, or `-1` for the last week | + | `week_interval` | `custom_week` | repeat every `2`–`12` weeks | + | `month_interval` | `custom_month` | repeat every `2`–`12` months | + +- `recurs_until` — top-level (sibling of `recurrence_schedule`), ISO 8601 + date the recurrence ends; omit to recur indefinitely. Reflected as + `recurrence_schedule.end_date` in the response. +- The remaining `recurrence_schedule` attributes shown in the read payload + (`hour`, `minute`, `start_date`, `duration`, `end_date`) are **derived** + from `starts_at`/`ends_at`/`recurs_until` and **ignored on input**. +- An **invalid `recurrence_schedule` is silently discarded on create**: the + entry is created without recurring (no validation error). +- Update: adding a `recurrence_schedule` makes a non-recurring entry recur. + An entry that **already recurs can't be changed** through the update + endpoint — it redirects to the entry's first occurrence, like Get a + schedule entry does. + +**Server-hang warning**: never send `week_instance: 0`. BC3 currently spins +computing occurrences for it (open BC3 **#12362**, "Reject recurrence +schedules whose occurrences can't be computed", adds the rejection). Until +#12362 merges, an SDK-side guard or enum constraint must make `0` +unrepresentable. + +## Why it matters + +Recurring events are table stakes for calendar integrations. The SDK can +read `recurrence_schedule` but can't create or update recurring entries, so +any integration syncing an external calendar into Basecamp has to fan a +recurring series out into individual entries — which then don't behave as a +series in the product. + +## Suggested API shape + +Additive optional members on the existing `CreateScheduleEntry` / +`UpdateScheduleEntry` inputs: a `recurrence_schedule` structure (enum-typed +`frequency`; integer list `days`; bounded integers per the table) plus +top-level `recurs_until: ISO8601Date`. Response shapes unchanged (the read +side already models `recurrence_schedule`). + +## Implementation notes for BC3 + +- Shipped in docs — `schedules/entries_controller.rb` handles the params. +- #12362 (open) is the server-side guard for uncomputable schedules + (`week_instance: 0` et al.); silent-discard-on-invalid on create is + documented, not a bug. + +## SDK absorption plan when this lands + +- Vehicle: the §Q absorption queue's **PR-5**. Docs are merged, so the doc + gate is cleared, but the **wire-echo probe remains a pre-modeling gate**: + before modeling, probe a live BC5 create with each frequency family and + diff the echoed `recurrence_schedule` against the doc (the derived-fields + and silent-discard semantics make doc-only modeling risky). +- Model `frequency` as an enum (nine values above); constrain or guard + `week_instance` so `0` can't be sent while BC3 #12362 is open. +- Status flips to `absorbed-in-sdk` with the absorption PR (which adds the + Smithy refs). +- Pairwise check: BC4 accepts the same create/update routes but recurrence + behavior on BC4 is unverified — probe both rails before asserting + pairwise invariants. diff --git a/spec/api-gaps/search-filter-additions.md b/spec/api-gaps/search-filter-additions.md index 6c0f774f8..645302c62 100644 --- a/spec/api-gaps/search-filter-additions.md +++ b/spec/api-gaps/search-filter-additions.md @@ -19,18 +19,27 @@ bc3_refs: ## What's missing -Docs shipped, params not final — **do not absorb yet**. Search filter -documentation landed on `master` with the BC5 API train (2026-07-18..21), but -open BC3 **#12361** (search params rework) is actively reshaping the filter -parameter surface. The status stays `partial-coverage` until #12361 settles: -absorbing the current param list would model a contract BC3 has already -queued for change. +Docs shipped, params not final — **do not absorb yet**. As of the 2026-07-22 +sync, `doc/api/sections/search.md` on `master` documents the filter surface: -The filter families in play (subject to #12361's rework — re-derive the final -list from `doc/api/sections/search.md` once it merges): +- `type_names[]` — array of recording types to include (`key` values from + the metadata endpoint's `recording_search_types`); the metadata endpoint + prose now advertises `type_names[]` as the discovery target. +- `bucket_ids[]` — array of project IDs. +- `creator_ids[]` — array of creator person IDs. +- `since` — time-range filter: `last_7_days`, `last_30_days`, `last_90_days`, + `last_12_months`, or `forever` (the default); unrecognized values + normalize to `forever`. +- `sort` — `best_match` (default, relevance with a recency boost) or + `recency` (strictly newest first); unrecognized values fall back to + recency ordering. +- Deprecated-but-retained singulars for older clients: `type`, `bucket_id`, + `creator_id` (prefer the plural array forms). -- recording-type filtering, creator/person filtering, project scoping, -- chat exclusion, file-type filtering, and result ordering. +The hold stands because open BC3 **#12361** ("Search API: query-faithful +params and root cache") is still reshaping this parameter surface. The +status stays `partial-coverage` until #12361 settles: absorbing the current +param list would model a contract BC3 has already queued for change. The `timelines/searches` route is the timeline-scoped variant; covered here since it shares the input shape. @@ -54,7 +63,8 @@ unchanged. controller actions, no new partials. - #12361 (open) is the deciding PR for the final param names/semantics; `doc/api/sections/search.md` follows it. -- Document defaults explicitly (e.g. the default sort). +- Defaults are now documented (`since=forever`, `sort=best_match`), along + with the fallback behavior for unrecognized values. ## SDK absorption plan when this lands diff --git a/spec/api-gaps/todoset-direct-todo-create.md b/spec/api-gaps/todoset-direct-todo-create.md new file mode 100644 index 000000000..1c8ac777b --- /dev/null +++ b/spec/api-gaps/todoset-direct-todo-create.md @@ -0,0 +1,66 @@ +--- +gap: todoset-direct-todo-create +status: addressed-in-bc3-pr-12359 +detected: 2026-07-22 +sdk_demand: medium +bc3_pr: 12359 +bc3_refs: + introduced_in: five + routes: + - "POST /:account_id/buckets/:project_id/todosets/:todoset_id/todos.json" + controllers: + - app/controllers/todos_controller.rb + related_existing_api: + - CreateTodo + - GetTodoset +--- + +# To-do set — create a to-do directly (outside any list) + +## What's missing + +SDK absorption only — the contract shipped via BC3 **#12359** (merged +2026-07-22, post-train follow-up to the BC5 API docs). "Create a to-do" in +`doc/api/sections/todos.md` on `master` now documents a second create form: + +- `POST /buckets/:project_id/todosets/:todoset_id/todos.json` creates a + to-do **directly under the to-do set**, outside of any to-do list. +- **This form is only available project-scoped** — there is no account-scoped + variant (unlike most BC5 canonical routes). +- Parameters and response match the existing to-do-list create: required + `content`, optional `description`/`assignee_ids`/`completion_subscriber_ids`/ + `notify`/`due_on`/`starts_on`, returns `201 Created` with the Todo payload. +- Find a project's to-do set via the existing Get to-do set endpoint + (`GetTodoset` in the SDK). + +## Why it matters + +Loose to-dos (to-dos that live directly on the to-do set rather than in a +list) are a BC5 feature. SDK consumers can see them in list/read surfaces +but can't create them — the modeled `CreateTodo` requires a to-do list id, +so integrations that mirror BC5's "add a to-do without picking a list" flow +have no API path. + +## Suggested API shape + +`CreateTodosetTodo` operation: +`POST /{accountId}/buckets/{projectId}/todosets/{todosetId}/todos.json`, +reusing `CreateTodo`'s payload members and the `Todo` output shape. `201` +with the created Todo. + +## Implementation notes for BC3 + +Shipped — nothing pending. Routed via `resources :todosets do resources +:todos, only: %i[index create]` in the bucket scope, served by +`todos_controller.rb`; the doc documents the create form. + +## SDK absorption plan when this lands + +- Vehicle: the §Q absorption queue's **PR-2 build-ahead pair** + (`UpdateCampfireLine` + `CreateTodosetTodo`), pre-approved. +- New Smithy operation `CreateTodosetTodo` sharing payload/output shapes + with `CreateTodo`; registers on the existing Todos (or Todosets) service. +- Status flips to `absorbed-in-sdk` with the absorption PR (which adds the + Smithy refs). +- Pairwise check: BC4 has no loose to-dos — expect 404 on BC4, 201 on BC5; + additive-only. From bbc5e5be42e774497d4d38ed9dc4c2101722b972 Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Wed, 22 Jul 2026 00:15:43 -0700 Subject: [PATCH 03/12] spec: modeled-surface doc/optionality fixes from the 2026-07-22 drift MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - OutOfOffice: add optional back_on_date (now documented) and document the disabled state — enabled=false with start/end/back_on dates omitted, not null. start_date/end_date were already optional. - GetMyNotifications: op doc now describes the BC5 grouping (unreads, reads, bubble-ups, scheduled bubble-ups; memories is an always-empty placeholder); Notification.section documents the current enum (inbox/chats/pings/bubbles/mentions — remembered was dropped). - Preferences: time_zone_name returned Rails-style; writes accept IANA identifiers and normalize before saving. - Webhook.recent_deliveries: up to 25 most recent, empty until first delivery. - GetUpcomingSchedule: canonical-path note (BC5 /calendar web view is HTML-only; route shape unchanged). Not changed, verified: EventDetails already models added_person_ids, removed_person_ids, and notified_recipient_ids (the event-example soft flag is already reconciled); Tool/DockItem never modeled the embedded service object, so the CloudFile::Service#code reliability fix has no modeled surface to annotate. --- go/pkg/generated/client.gen.go | 67 ++++++++++++------- .../sdk/generated/services/reports.kt | 2 +- openapi.json | 20 ++++-- python/src/basecamp/generated/types.py | 1 + .../generated/services/reports_service.rb | 2 +- ruby/lib/basecamp/generated/types.rb | 6 +- spec/basecamp.smithy | 29 ++++++-- .../Generated/Models/OutOfOffice.swift | 1 + .../src/generated/openapi-stripped.json | 20 ++++-- typescript/src/generated/schema.d.ts | 37 ++++++++-- typescript/src/generated/services/reports.ts | 2 +- 11 files changed, 137 insertions(+), 50 deletions(-) diff --git a/go/pkg/generated/client.gen.go b/go/pkg/generated/client.gen.go index dfa83a262..26344771d 100644 --- a/go/pkg/generated/client.gen.go +++ b/go/pkg/generated/client.gen.go @@ -781,7 +781,8 @@ type EnableOutOfOfficeRequestContent struct { OutOfOffice OutOfOfficePayload `json:"out_of_office"` } -// EnableOutOfOfficeResponseContent defines model for EnableOutOfOfficeResponseContent. +// EnableOutOfOfficeResponseContent When out of office is not enabled, `enabled` is `false` and +// `start_date`, `end_date`, and `back_on_date` are omitted. type EnableOutOfOfficeResponseContent = OutOfOffice // Event defines model for Event. @@ -1047,7 +1048,8 @@ type GetMyPreferencesResponseContent = Preferences // GetMyProfileResponseContent defines model for GetMyProfileResponseContent. type GetMyProfileResponseContent = Person -// GetOutOfOfficeResponseContent defines model for GetOutOfOfficeResponseContent. +// GetOutOfOfficeResponseContent When out of office is not enabled, `enabled` is `false` and +// `start_date`, `end_date`, and `back_on_date` are omitted. type GetOutOfOfficeResponseContent = OutOfOffice // GetOverdueTodosResponseContent defines model for GetOverdueTodosResponseContent. @@ -1473,24 +1475,31 @@ type Notification struct { ReadAt time.Time `json:"read_at,omitempty"` ReadableIdentifier string `json:"readable_identifier,omitempty"` ReadableSgid string `json:"readable_sgid,omitempty"` - Section string `json:"section,omitempty"` - Subscribed bool `json:"subscribed,omitempty"` - SubscriptionUrl string `json:"subscription_url,omitempty"` - Title string `json:"title,omitempty"` - Type string `json:"type,omitempty"` - UnreadAt time.Time `json:"unread_at,omitempty"` - UnreadCount int32 `json:"unread_count,omitempty"` - UnreadUrl string `json:"unread_url,omitempty"` - UpdatedAt time.Time `json:"updated_at"` -} - -// OutOfOffice defines model for OutOfOffice. + + // Section The notification category: `inbox`, `chats`, `pings`, `bubbles`, + // or `mentions`. + Section string `json:"section,omitempty"` + Subscribed bool `json:"subscribed,omitempty"` + SubscriptionUrl string `json:"subscription_url,omitempty"` + Title string `json:"title,omitempty"` + Type string `json:"type,omitempty"` + UnreadAt time.Time `json:"unread_at,omitempty"` + UnreadCount int32 `json:"unread_count,omitempty"` + UnreadUrl string `json:"unread_url,omitempty"` + UpdatedAt time.Time `json:"updated_at"` +} + +// OutOfOffice When out of office is not enabled, `enabled` is `false` and +// `start_date`, `end_date`, and `back_on_date` are omitted. type OutOfOffice struct { - Enabled bool `json:"enabled,omitempty"` - EndDate string `json:"end_date,omitempty"` - Ongoing bool `json:"ongoing,omitempty"` - Person OutOfOfficePerson `json:"person,omitempty"` - StartDate string `json:"start_date,omitempty"` + // BackOnDate First working day after the out-of-office window ends. + // Omitted when out of office is not enabled. + BackOnDate string `json:"back_on_date,omitempty"` + Enabled bool `json:"enabled,omitempty"` + EndDate string `json:"end_date,omitempty"` + Ongoing bool `json:"ongoing,omitempty"` + Person OutOfOfficePerson `json:"person,omitempty"` + StartDate string `json:"start_date,omitempty"` } // OutOfOfficePayload defines model for OutOfOfficePayload. @@ -1554,6 +1563,8 @@ type Preferences struct { AppUrl string `json:"app_url,omitempty"` FirstWeekDay string `json:"first_week_day,omitempty"` TimeFormat string `json:"time_format,omitempty"` + + // TimeZoneName Returned as a Rails-style name (e.g. "Central Time (US & Canada)"). TimeZoneName string `json:"time_zone_name,omitempty"` Url string `json:"url,omitempty"` } @@ -1566,7 +1577,10 @@ type PreferencesPayload struct { // TimeFormat Time display format: twelve_hour or twenty_four_hour TimeFormat string `json:"time_format,omitempty"` - // TimeZoneName Time zone name (e.g. "America/Chicago", "London", "UTC") + // TimeZoneName Time zone name. Accepts any valid Rails time zone name (e.g. + // "London", "UTC") as well as IANA identifiers (e.g. + // "America/Chicago"), which are normalized to the matching + // Rails-style name before saving. TimeZoneName string `json:"time_zone_name,omitempty"` } @@ -2520,11 +2534,14 @@ type Vault struct { // Webhook defines model for Webhook. type Webhook struct { - Active bool `json:"active,omitempty"` - AppUrl string `json:"app_url"` - CreatedAt time.Time `json:"created_at"` - Id int64 `json:"id"` - PayloadUrl string `json:"payload_url"` + Active bool `json:"active,omitempty"` + AppUrl string `json:"app_url"` + CreatedAt time.Time `json:"created_at"` + Id int64 `json:"id"` + PayloadUrl string `json:"payload_url"` + + // RecentDeliveries Up to the 25 most recent delivery exchanges, most recent first. + // Empty when the webhook hasn't delivered anything yet. RecentDeliveries []WebhookDelivery `json:"recent_deliveries,omitempty"` Types []string `json:"types,omitempty"` UpdatedAt time.Time `json:"updated_at"` diff --git a/kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/generated/services/reports.kt b/kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/generated/services/reports.kt index cbce2557b..481ac698a 100644 --- a/kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/generated/services/reports.kt +++ b/kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/generated/services/reports.kt @@ -41,7 +41,7 @@ class ReportsService(client: AccountClient) : BaseService(client) { } /** - * Get upcoming schedule entries within a date window + * Get upcoming schedule entries and assignable items within a date * @param options Optional query parameters and pagination control */ suspend fun upcoming(options: GetUpcomingScheduleOptions? = null): JsonElement { diff --git a/openapi.json b/openapi.json index 29ab71d06..a1be39cc0 100644 --- a/openapi.json +++ b/openapi.json @@ -8790,7 +8790,7 @@ }, "/{accountId}/my/readings.json": { "get": { - "description": "Get the current user's notification inbox (the \"Hey!\" menu).\nNotifications are grouped into unreads, reads, and memories.\nReads are paginated (50 per page). Unreads are capped at 100.", + "description": "Get the current user's notification inbox (the \"Hey!\" menu).\nNotifications are grouped into unreads, reads, bubble-ups, and\nscheduled bubble-ups (`memories` remains as an always-empty\nplaceholder on BC5). Reads are paginated (50 per page). Unreads are\ncapped at 100. Bubble-ups are capped per `limit_bubble_ups`.", "operationId": "GetMyNotifications", "parameters": [ { @@ -14950,7 +14950,7 @@ }, "/{accountId}/reports/schedules/upcoming.json": { "get": { - "description": "Get upcoming schedule entries within a date window", + "description": "Get upcoming schedule entries and assignable items within a date\nwindow. This endpoint is preserved as the canonical API path on BC5;\nthe BC5 `/calendar` web view is HTML-only.", "operationId": "GetUpcomingSchedule", "parameters": [ { @@ -24654,7 +24654,8 @@ "x-go-type-skip-optional-pointer": true }, "section": { - "type": "string" + "type": "string", + "description": "The notification category: `inbox`, `chats`, `pings`, `bubbles`,\nor `mentions`." }, "unread_count": { "type": "integer", @@ -24758,6 +24759,7 @@ }, "OutOfOffice": { "type": "object", + "description": "When out of office is not enabled, `enabled` is `false` and\n`start_date`, `end_date`, and `back_on_date` are omitted.", "properties": { "person": { "$ref": "#/components/schemas/OutOfOfficePerson" @@ -24773,6 +24775,10 @@ }, "end_date": { "type": "string" + }, + "back_on_date": { + "type": "string", + "description": "First working day after the out-of-office window ends.\nOmitted when out of office is not enabled." } } }, @@ -24976,7 +24982,8 @@ "type": "string" }, "time_zone_name": { - "type": "string" + "type": "string", + "description": "Returned as a Rails-style name (e.g. \"Central Time (US & Canada)\")." }, "first_week_day": { "type": "string" @@ -24991,7 +24998,7 @@ "properties": { "time_zone_name": { "type": "string", - "description": "Time zone name (e.g. \"America/Chicago\", \"London\", \"UTC\")" + "description": "Time zone name. Accepts any valid Rails time zone name (e.g.\n\"London\", \"UTC\") as well as IANA identifiers (e.g.\n\"America/Chicago\"), which are normalized to the matching\nRails-style name before saving." }, "first_week_day": { "type": "string", @@ -27880,7 +27887,8 @@ "type": "array", "items": { "$ref": "#/components/schemas/WebhookDelivery" - } + }, + "description": "Up to the 25 most recent delivery exchanges, most recent first.\nEmpty when the webhook hasn't delivered anything yet." } }, "required": [ diff --git a/python/src/basecamp/generated/types.py b/python/src/basecamp/generated/types.py index 49082a158..d1313ec3c 100644 --- a/python/src/basecamp/generated/types.py +++ b/python/src/basecamp/generated/types.py @@ -919,6 +919,7 @@ class Notification(TypedDict): class OutOfOffice(TypedDict): + back_on_date: NotRequired[str] enabled: NotRequired[bool] end_date: NotRequired[str] ongoing: NotRequired[bool] diff --git a/ruby/lib/basecamp/generated/services/reports_service.rb b/ruby/lib/basecamp/generated/services/reports_service.rb index bc7cae28b..d8c69056a 100644 --- a/ruby/lib/basecamp/generated/services/reports_service.rb +++ b/ruby/lib/basecamp/generated/services/reports_service.rb @@ -15,7 +15,7 @@ def progress() end end - # Get upcoming schedule entries within a date window + # Get upcoming schedule entries and assignable items within a date # @param window_starts_on [String, nil] window starts on # @param window_ends_on [String, nil] window ends on # @return [Hash] response data diff --git a/ruby/lib/basecamp/generated/types.rb b/ruby/lib/basecamp/generated/types.rb index 7f048b2f5..eef1bd576 100644 --- a/ruby/lib/basecamp/generated/types.rb +++ b/ruby/lib/basecamp/generated/types.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # Auto-generated from OpenAPI spec. Do not edit manually. -# Generated: 2026-07-22T02:53:58Z +# Generated: 2026-07-22T07:14:48Z require "json" require "time" @@ -2168,9 +2168,10 @@ def to_json(*args) # OutOfOffice class OutOfOffice include TypeHelpers - attr_accessor :enabled, :end_date, :ongoing, :person, :start_date + attr_accessor :back_on_date, :enabled, :end_date, :ongoing, :person, :start_date def initialize(data = {}) + @back_on_date = data["back_on_date"] @enabled = parse_boolean(data["enabled"]) @end_date = data["end_date"] @ongoing = parse_boolean(data["ongoing"]) @@ -2180,6 +2181,7 @@ def initialize(data = {}) def to_h { + "back_on_date" => @back_on_date, "enabled" => @enabled, "end_date" => @end_date, "ongoing" => @ongoing, diff --git a/spec/basecamp.smithy b/spec/basecamp.smithy index 01db7659b..91e2fc466 100644 --- a/spec/basecamp.smithy +++ b/spec/basecamp.smithy @@ -5822,6 +5822,9 @@ structure Webhook { url: String @required app_url: String + + /// Up to the 25 most recent delivery exchanges, most recent first. + /// Empty when the webhook hasn't delivered anything yet. recent_deliveries: WebhookDeliveryList } @@ -7223,7 +7226,9 @@ structure GetOverdueTodosOutput { over_three_months_late: TodoItems } -/// Get upcoming schedule entries within a date window +/// Get upcoming schedule entries and assignable items within a date +/// window. This endpoint is preserved as the canonical API path on BC5; +/// the BC5 `/calendar` web view is HTML-only. @readonly @basecampRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) @http(method: "GET", uri: "/{accountId}/reports/schedules/upcoming.json") @@ -8163,8 +8168,10 @@ list MyAssignmentAssigneeList { // ===== Notification Operations ===== /// Get the current user's notification inbox (the "Hey!" menu). -/// Notifications are grouped into unreads, reads, and memories. -/// Reads are paginated (50 per page). Unreads are capped at 100. +/// Notifications are grouped into unreads, reads, bubble-ups, and +/// scheduled bubble-ups (`memories` remains as an always-empty +/// placeholder on BC5). Reads are paginated (50 per page). Unreads are +/// capped at 100. Bubble-ups are capped per `limit_bubble_ups`. @readonly @basecampRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) @http(method: "GET", uri: "/{accountId}/my/readings.json") @@ -8246,6 +8253,9 @@ structure Notification { created_at: ISO8601Timestamp @required updated_at: ISO8601Timestamp + + /// The notification category: `inbox`, `chats`, `pings`, `bubbles`, + /// or `mentions`. section: String unread_count: Integer unread_at: ISO8601Timestamp @@ -8391,12 +8401,18 @@ structure DisableOutOfOfficeOutput {} // ===== Out of Office Shapes ===== +/// When out of office is not enabled, `enabled` is `false` and +/// `start_date`, `end_date`, and `back_on_date` are omitted. structure OutOfOffice { person: OutOfOfficePerson enabled: Boolean ongoing: Boolean start_date: ISO8601Date end_date: ISO8601Date + + /// First working day after the out-of-office window ends. + /// Omitted when out of office is not enabled. + back_on_date: ISO8601Date } structure OutOfOfficePerson { @@ -8453,7 +8469,10 @@ structure UpdateMyPreferencesInput { } structure PreferencesPayload { - /// Time zone name (e.g. "America/Chicago", "London", "UTC") + /// Time zone name. Accepts any valid Rails time zone name (e.g. + /// "London", "UTC") as well as IANA identifiers (e.g. + /// "America/Chicago"), which are normalized to the matching + /// Rails-style name before saving. time_zone_name: String /// First day of the week: Sunday, Monday, Tuesday, etc. @@ -8473,6 +8492,8 @@ structure UpdateMyPreferencesOutput { structure Preferences { url: String app_url: String + + /// Returned as a Rails-style name (e.g. "Central Time (US & Canada)"). time_zone_name: String first_week_day: String time_format: String diff --git a/swift/Sources/Basecamp/Generated/Models/OutOfOffice.swift b/swift/Sources/Basecamp/Generated/Models/OutOfOffice.swift index ca0a0170d..fcc738d8e 100644 --- a/swift/Sources/Basecamp/Generated/Models/OutOfOffice.swift +++ b/swift/Sources/Basecamp/Generated/Models/OutOfOffice.swift @@ -2,6 +2,7 @@ import Foundation public struct OutOfOffice: Codable, Sendable { + public var backOnDate: String? public var enabled: Bool? public var endDate: String? public var ongoing: Bool? diff --git a/typescript/src/generated/openapi-stripped.json b/typescript/src/generated/openapi-stripped.json index 008a2c714..d91514146 100644 --- a/typescript/src/generated/openapi-stripped.json +++ b/typescript/src/generated/openapi-stripped.json @@ -7804,7 +7804,7 @@ }, "/my/readings.json": { "get": { - "description": "Get the current user's notification inbox (the \"Hey!\" menu).\nNotifications are grouped into unreads, reads, and memories.\nReads are paginated (50 per page). Unreads are capped at 100.", + "description": "Get the current user's notification inbox (the \"Hey!\" menu).\nNotifications are grouped into unreads, reads, bubble-ups, and\nscheduled bubble-ups (`memories` remains as an always-empty\nplaceholder on BC5). Reads are paginated (50 per page). Unreads are\ncapped at 100. Bubble-ups are capped per `limit_bubble_ups`.", "operationId": "GetMyNotifications", "parameters": [ { @@ -13258,7 +13258,7 @@ }, "/reports/schedules/upcoming.json": { "get": { - "description": "Get upcoming schedule entries within a date window", + "description": "Get upcoming schedule entries and assignable items within a date\nwindow. This endpoint is preserved as the canonical API path on BC5;\nthe BC5 `/calendar` web view is HTML-only.", "operationId": "GetUpcomingSchedule", "parameters": [ { @@ -22330,7 +22330,8 @@ "x-go-type-skip-optional-pointer": true }, "section": { - "type": "string" + "type": "string", + "description": "The notification category: `inbox`, `chats`, `pings`, `bubbles`,\nor `mentions`." }, "unread_count": { "type": "integer", @@ -22434,6 +22435,7 @@ }, "OutOfOffice": { "type": "object", + "description": "When out of office is not enabled, `enabled` is `false` and\n`start_date`, `end_date`, and `back_on_date` are omitted.", "properties": { "person": { "$ref": "#/components/schemas/OutOfOfficePerson" @@ -22449,6 +22451,10 @@ }, "end_date": { "type": "string" + }, + "back_on_date": { + "type": "string", + "description": "First working day after the out-of-office window ends.\nOmitted when out of office is not enabled." } } }, @@ -22652,7 +22658,8 @@ "type": "string" }, "time_zone_name": { - "type": "string" + "type": "string", + "description": "Returned as a Rails-style name (e.g. \"Central Time (US & Canada)\")." }, "first_week_day": { "type": "string" @@ -22667,7 +22674,7 @@ "properties": { "time_zone_name": { "type": "string", - "description": "Time zone name (e.g. \"America/Chicago\", \"London\", \"UTC\")" + "description": "Time zone name. Accepts any valid Rails time zone name (e.g.\n\"London\", \"UTC\") as well as IANA identifiers (e.g.\n\"America/Chicago\"), which are normalized to the matching\nRails-style name before saving." }, "first_week_day": { "type": "string", @@ -25556,7 +25563,8 @@ "type": "array", "items": { "$ref": "#/components/schemas/WebhookDelivery" - } + }, + "description": "Up to the 25 most recent delivery exchanges, most recent first.\nEmpty when the webhook hasn't delivered anything yet." } }, "required": [ diff --git a/typescript/src/generated/schema.d.ts b/typescript/src/generated/schema.d.ts index b2a4fff27..efa0f84c9 100644 --- a/typescript/src/generated/schema.d.ts +++ b/typescript/src/generated/schema.d.ts @@ -1104,8 +1104,10 @@ export interface paths { }; /** * @description Get the current user's notification inbox (the "Hey!" menu). - * Notifications are grouped into unreads, reads, and memories. - * Reads are paginated (50 per page). Unreads are capped at 100. + * Notifications are grouped into unreads, reads, bubble-ups, and + * scheduled bubble-ups (`memories` remains as an always-empty + * placeholder on BC5). Reads are paginated (50 per page). Unreads are + * capped at 100. Bubble-ups are capped per `limit_bubble_ups`. */ get: operations["GetMyNotifications"]; put?: never; @@ -1848,7 +1850,11 @@ export interface paths { path?: never; cookie?: never; }; - /** @description Get upcoming schedule entries within a date window */ + /** + * @description Get upcoming schedule entries and assignable items within a date + * window. This endpoint is preserved as the canonical API path on BC5; + * the BC5 `/calendar` web view is HTML-only. + */ get: operations["GetUpcomingSchedule"]; put?: never; post?: never; @@ -3580,6 +3586,10 @@ export interface components { id: number; created_at: string; updated_at: string; + /** + * @description The notification category: `inbox`, `chats`, `pings`, `bubbles`, + * or `mentions`. + */ section?: string; /** Format: int32 */ unread_count?: number; @@ -3616,12 +3626,21 @@ export interface components { /** @description Custom image URL (pings only) */ image_url?: string; }; + /** + * @description When out of office is not enabled, `enabled` is `false` and + * `start_date`, `end_date`, and `back_on_date` are omitted. + */ OutOfOffice: { person?: components["schemas"]["OutOfOfficePerson"]; enabled?: boolean; ongoing?: boolean; start_date?: string; end_date?: string; + /** + * @description First working day after the out-of-office window ends. + * Omitted when out of office is not enabled. + */ + back_on_date?: string; }; OutOfOfficePayload: { /** @description Start date in ISO 8601 format (YYYY-MM-DD) */ @@ -3686,12 +3705,18 @@ export interface components { Preferences: { url?: string; app_url?: string; + /** @description Returned as a Rails-style name (e.g. "Central Time (US & Canada)"). */ time_zone_name?: string; first_week_day?: string; time_format?: string; }; PreferencesPayload: { - /** @description Time zone name (e.g. "America/Chicago", "London", "UTC") */ + /** + * @description Time zone name. Accepts any valid Rails time zone name (e.g. + * "London", "UTC") as well as IANA identifiers (e.g. + * "America/Chicago"), which are normalized to the matching + * Rails-style name before saving. + */ time_zone_name?: string; /** @description First day of the week: Sunday, Monday, Tuesday, etc. */ first_week_day?: string; @@ -4515,6 +4540,10 @@ export interface components { types?: string[]; url: string; app_url: string; + /** + * @description Up to the 25 most recent delivery exchanges, most recent first. + * Empty when the webhook hasn't delivered anything yet. + */ recent_deliveries?: components["schemas"]["WebhookDelivery"][]; }; /** @description Reference to a copied/moved recording in copy events. */ diff --git a/typescript/src/generated/services/reports.ts b/typescript/src/generated/services/reports.ts index 45fc6a819..852c81f72 100644 --- a/typescript/src/generated/services/reports.ts +++ b/typescript/src/generated/services/reports.ts @@ -84,7 +84,7 @@ export class ReportsService extends BaseService { } /** - * Get upcoming schedule entries within a date window + * Get upcoming schedule entries and assignable items within a date * @param options - Optional query parameters * @returns The upcoming_schedule * From eeca078dcfd1ecf8f5547198f185920ff83a0cb7 Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Wed, 22 Jul 2026 00:16:42 -0700 Subject: [PATCH 04/12] docs: codify provenance pin semantics; refresh coordination note MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AGENTS.md §Provenance is Mandatory gains the pin-semantics paragraph: the pin asserts drift up to that revision is triaged (absorbed or registered in spec/api-gaps/), not that everything in it is absorbed; a repin is valid when every pin..HEAD drift item is one or the other; the pin never moves backward; compatibility.* pins mark the last verified API-surface state of their branch, not a timestamp. This is the reading the registry and its tooling already implement, and it unblocks reviewer agents that read the AGENTS.md:265 sync rule as requiring HEAD-currency on every sync-shaped PR. COORDINATION.md: contract-authority note now names the current pin (ba105ba7, 2026-07-22) and records the four-branch re-verification — zero API drift since 9d73959a (3 commits, none under doc/api or the API controllers/views). --- AGENTS.md | 2 ++ COORDINATION.md | 9 +++++++++ 2 files changed, 11 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index f8d4c279e..9b980fb40 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -271,6 +271,8 @@ Update the `revision` and `date` fields, then `make provenance-sync`. This is no The Smithy service version is derived from the shared provenance date. Run `make sync-spec-version` (or `make smithy-build`, which does this automatically) after updating provenance. +**Pin semantics.** The pin is the conformance baseline as of the last sync — it asserts that all upstream drift up to that revision has been *triaged*, not that every contract in it has been *absorbed*. Upstream contracts shipped past the SDK's modeled surface are tracked in `spec/api-gaps/` (status `addressed-in-bc3-pr-N`) until an absorption PR lands. A repin is valid exactly when every drift item in `pin..HEAD` is either absorbed into the spec or registered in `spec/api-gaps/`; it is not blocked on absorption itself. The pin never moves backward. The `compatibility.*` pins mark the last **verified API-surface state** of their branch, not a last-glanced timestamp — refresh one only when re-verifying that branch's API surface, and record verification dates in the PR that did the checking. + ### Pre-sync Use `make sync-status` to see upstream diffs since last sync. diff --git a/COORDINATION.md b/COORDINATION.md index bde908154..0f9126c74 100644 --- a/COORDINATION.md +++ b/COORDINATION.md @@ -7,6 +7,15 @@ live BC5 by the #11629 tooling. The historical server-side audit lived on the `five+api` branch under `tmp/api_audit.md` (generated by the BC3 plan's `script/api/audit`). +The SDK's conformance baseline is the pin in +[`spec/api-provenance.json`](spec/api-provenance.json) — `bc3` `master` +`ba105ba7` as of the 2026-07-22 sync. Contracts documented past the SDK's +modeled surface are registered in [`spec/api-gaps/`](spec/api-gaps/) until +absorbed (see AGENTS.md §Provenance is Mandatory for the pin semantics). +The `compatibility.bc3-four` pin stays at `9d73959a` (2026-06-12): the +`four` branch was re-verified on 2026-07-22 with **zero API drift** since — +no changes under `doc/api/` or the API controllers/views. + ## Division of labor - **BC3 plan owns**: server-side audit, jbuilder + controller + doc From 19361154bb5d1226ca1769b3bbcecdd89c124176 Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Wed, 22 Jul 2026 00:18:21 -0700 Subject: [PATCH 05/12] go: propagate back_on_date on the OutOfOffice wrapper Follows the spec-side addition; the wrapper unmarshals the raw body, so the tagged field is the whole propagation. Documents the disabled-state omission alongside. Clears check-wrapper-drift. --- go/pkg/basecamp/people.go | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/go/pkg/basecamp/people.go b/go/pkg/basecamp/people.go index 57e80458f..2d35cb169 100644 --- a/go/pkg/basecamp/people.go +++ b/go/pkg/basecamp/people.go @@ -534,12 +534,15 @@ type UpdateMyPreferencesRequest struct { } // OutOfOffice represents out-of-office status for a person. +// When out of office is not enabled, Enabled is false and StartDate, +// EndDate, and BackOnDate are omitted from the response. type OutOfOffice struct { - Enabled bool `json:"enabled,omitempty"` - StartDate string `json:"start_date,omitempty"` - EndDate string `json:"end_date,omitempty"` - Ongoing bool `json:"ongoing,omitempty"` - Person OutOfOfficePerson `json:"person,omitempty"` + Enabled bool `json:"enabled,omitempty"` + StartDate string `json:"start_date,omitempty"` + EndDate string `json:"end_date,omitempty"` + BackOnDate string `json:"back_on_date,omitempty"` + Ongoing bool `json:"ongoing,omitempty"` + Person OutOfOfficePerson `json:"person,omitempty"` } // OutOfOfficePerson represents the person associated with an out-of-office status. From b61843b55b160dbb250ee8f0477d2e8704ad7b69 Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Wed, 22 Jul 2026 00:32:17 -0700 Subject: [PATCH 06/12] spec: keep GetUpcomingSchedule doc summary on one line TS/Ruby/Kotlin service-doc generators take only the first doc line; the reflowed summary was truncating at 'within a date'. First line is now the complete sentence; the BC5 canonical-path note follows. --- .../kotlin/com/basecamp/sdk/generated/services/reports.kt | 2 +- openapi.json | 2 +- ruby/lib/basecamp/generated/services/reports_service.rb | 2 +- spec/basecamp.smithy | 4 ++-- typescript/src/generated/openapi-stripped.json | 2 +- typescript/src/generated/schema.d.ts | 4 ++-- typescript/src/generated/services/reports.ts | 2 +- 7 files changed, 9 insertions(+), 9 deletions(-) diff --git a/kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/generated/services/reports.kt b/kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/generated/services/reports.kt index 481ac698a..0f9b9d77f 100644 --- a/kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/generated/services/reports.kt +++ b/kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/generated/services/reports.kt @@ -41,7 +41,7 @@ class ReportsService(client: AccountClient) : BaseService(client) { } /** - * Get upcoming schedule entries and assignable items within a date + * Get upcoming schedule entries and assignable items within a date window. * @param options Optional query parameters and pagination control */ suspend fun upcoming(options: GetUpcomingScheduleOptions? = null): JsonElement { diff --git a/openapi.json b/openapi.json index a1be39cc0..05321925f 100644 --- a/openapi.json +++ b/openapi.json @@ -14950,7 +14950,7 @@ }, "/{accountId}/reports/schedules/upcoming.json": { "get": { - "description": "Get upcoming schedule entries and assignable items within a date\nwindow. This endpoint is preserved as the canonical API path on BC5;\nthe BC5 `/calendar` web view is HTML-only.", + "description": "Get upcoming schedule entries and assignable items within a date window.\nThis endpoint is preserved as the canonical API path on BC5;\nthe BC5 `/calendar` web view is HTML-only.", "operationId": "GetUpcomingSchedule", "parameters": [ { diff --git a/ruby/lib/basecamp/generated/services/reports_service.rb b/ruby/lib/basecamp/generated/services/reports_service.rb index d8c69056a..d26f620dd 100644 --- a/ruby/lib/basecamp/generated/services/reports_service.rb +++ b/ruby/lib/basecamp/generated/services/reports_service.rb @@ -15,7 +15,7 @@ def progress() end end - # Get upcoming schedule entries and assignable items within a date + # Get upcoming schedule entries and assignable items within a date window. # @param window_starts_on [String, nil] window starts on # @param window_ends_on [String, nil] window ends on # @return [Hash] response data diff --git a/spec/basecamp.smithy b/spec/basecamp.smithy index 91e2fc466..b996f38d0 100644 --- a/spec/basecamp.smithy +++ b/spec/basecamp.smithy @@ -7226,8 +7226,8 @@ structure GetOverdueTodosOutput { over_three_months_late: TodoItems } -/// Get upcoming schedule entries and assignable items within a date -/// window. This endpoint is preserved as the canonical API path on BC5; +/// Get upcoming schedule entries and assignable items within a date window. +/// This endpoint is preserved as the canonical API path on BC5; /// the BC5 `/calendar` web view is HTML-only. @readonly @basecampRetry(maxAttempts: 3, baseDelayMs: 1000, backoff: "exponential", retryOn: [429, 503]) diff --git a/typescript/src/generated/openapi-stripped.json b/typescript/src/generated/openapi-stripped.json index d91514146..2e49e4f5e 100644 --- a/typescript/src/generated/openapi-stripped.json +++ b/typescript/src/generated/openapi-stripped.json @@ -13258,7 +13258,7 @@ }, "/reports/schedules/upcoming.json": { "get": { - "description": "Get upcoming schedule entries and assignable items within a date\nwindow. This endpoint is preserved as the canonical API path on BC5;\nthe BC5 `/calendar` web view is HTML-only.", + "description": "Get upcoming schedule entries and assignable items within a date window.\nThis endpoint is preserved as the canonical API path on BC5;\nthe BC5 `/calendar` web view is HTML-only.", "operationId": "GetUpcomingSchedule", "parameters": [ { diff --git a/typescript/src/generated/schema.d.ts b/typescript/src/generated/schema.d.ts index efa0f84c9..05900123f 100644 --- a/typescript/src/generated/schema.d.ts +++ b/typescript/src/generated/schema.d.ts @@ -1851,8 +1851,8 @@ export interface paths { cookie?: never; }; /** - * @description Get upcoming schedule entries and assignable items within a date - * window. This endpoint is preserved as the canonical API path on BC5; + * @description Get upcoming schedule entries and assignable items within a date window. + * This endpoint is preserved as the canonical API path on BC5; * the BC5 `/calendar` web view is HTML-only. */ get: operations["GetUpcomingSchedule"]; diff --git a/typescript/src/generated/services/reports.ts b/typescript/src/generated/services/reports.ts index 852c81f72..1791bb91d 100644 --- a/typescript/src/generated/services/reports.ts +++ b/typescript/src/generated/services/reports.ts @@ -84,7 +84,7 @@ export class ReportsService extends BaseService { } /** - * Get upcoming schedule entries and assignable items within a date + * Get upcoming schedule entries and assignable items within a date window. * @param options - Optional query parameters * @returns The upcoming_schedule * From f547e9b44780a5ca1eeeb59b1decde88bb0ac82a Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Wed, 22 Jul 2026 00:38:53 -0700 Subject: [PATCH 07/12] test: cover back_on_date flow-through for out-of-office Completeness bar: a changed field needs at least one fixture that proves it flows through. Adds spec/fixtures/people/out-of-office.json (values from the pinned doc example) plus enabled- and disabled-state tests at each layer that models the response: Go wrapper unmarshal, TS generated service via MSW, Ruby generated service via webmock. Disabled-state cases pin the omitted-not-null contract. --- go/pkg/basecamp/people_test.go | 48 +++++++++++++++++++ .../basecamp/services/people_service_test.rb | 31 ++++++++++++ spec/fixtures/people/out-of-office.json | 12 +++++ typescript/tests/services/people.test.ts | 39 +++++++++++++++ 4 files changed, 130 insertions(+) create mode 100644 spec/fixtures/people/out-of-office.json diff --git a/go/pkg/basecamp/people_test.go b/go/pkg/basecamp/people_test.go index d29f25187..cd0f4721c 100644 --- a/go/pkg/basecamp/people_test.go +++ b/go/pkg/basecamp/people_test.go @@ -440,3 +440,51 @@ func TestPeopleService_UpdateMyProfileClearsField(t *testing.T) { t.Errorf("expected bio to be empty string, got %v", bio) } } + +func TestOutOfOffice_Unmarshal(t *testing.T) { + data := loadPeopleFixture(t, "out-of-office.json") + + var ooo OutOfOffice + if err := json.Unmarshal(data, &ooo); err != nil { + t.Fatalf("failed to unmarshal out-of-office.json: %v", err) + } + + if !ooo.Enabled { + t.Error("expected enabled to be true") + } + if !ooo.Ongoing { + t.Error("expected ongoing to be true") + } + if ooo.StartDate != "2026-07-20" { + t.Errorf("expected start_date '2026-07-20', got %q", ooo.StartDate) + } + if ooo.EndDate != "2026-07-26" { + t.Errorf("expected end_date '2026-07-26', got %q", ooo.EndDate) + } + if ooo.BackOnDate != "2026-07-27" { + t.Errorf("expected back_on_date '2026-07-27', got %q", ooo.BackOnDate) + } + if ooo.Person.ID != 1049715913 { + t.Errorf("expected person id 1049715913, got %d", ooo.Person.ID) + } + if ooo.Person.Name != "Victor Cooper" { + t.Errorf("expected person name 'Victor Cooper', got %q", ooo.Person.Name) + } +} + +func TestOutOfOffice_UnmarshalDisabled(t *testing.T) { + data := []byte(`{"person":{"id":1049715913,"name":"Victor Cooper"},"enabled":false,"ongoing":false}`) + + var ooo OutOfOffice + if err := json.Unmarshal(data, &ooo); err != nil { + t.Fatalf("failed to unmarshal disabled out-of-office: %v", err) + } + + if ooo.Enabled { + t.Error("expected enabled to be false") + } + if ooo.StartDate != "" || ooo.EndDate != "" || ooo.BackOnDate != "" { + t.Errorf("expected omitted dates to be empty, got start=%q end=%q back_on=%q", + ooo.StartDate, ooo.EndDate, ooo.BackOnDate) + } +} diff --git a/ruby/test/basecamp/services/people_service_test.rb b/ruby/test/basecamp/services/people_service_test.rb index 3a4d67264..3705d26e7 100644 --- a/ruby/test/basecamp/services/people_service_test.rb +++ b/ruby/test/basecamp/services/people_service_test.rb @@ -98,4 +98,35 @@ def test_list_assignable assert_kind_of Array, result assert_equal 2, result.length end + + def test_get_out_of_office + stub_get("/12345/people/1/out_of_office.json", response_body: { + "person" => { "id" => 1049715913, "name" => "Victor Cooper" }, + "enabled" => true, + "ongoing" => true, + "start_date" => "2026-07-20", + "end_date" => "2026-07-26", + "back_on_date" => "2026-07-27" + }) + + result = @account.people.get_out_of_office(person_id: 1) + + assert result["enabled"] + assert_equal "2026-07-20", result["start_date"] + assert_equal "2026-07-26", result["end_date"] + assert_equal "2026-07-27", result["back_on_date"] + end + + def test_get_out_of_office_disabled_omits_dates + stub_get("/12345/people/1/out_of_office.json", response_body: { + "person" => { "id" => 1049715913, "name" => "Victor Cooper" }, + "enabled" => false, + "ongoing" => false + }) + + result = @account.people.get_out_of_office(person_id: 1) + + refute result["enabled"] + refute result.key?("back_on_date") + end end diff --git a/spec/fixtures/people/out-of-office.json b/spec/fixtures/people/out-of-office.json new file mode 100644 index 000000000..e510806a7 --- /dev/null +++ b/spec/fixtures/people/out-of-office.json @@ -0,0 +1,12 @@ +{ + "person": { + "id": 1049715913, + "name": "Victor Cooper", + "avatar_url": "https://3.basecampapi.com/195539477/people/BAhpBMlkkT4=--5fe7b70fbee7a7f0e2e1e19df7579e5d880c753d/avatar" + }, + "enabled": true, + "ongoing": true, + "start_date": "2026-07-20", + "end_date": "2026-07-26", + "back_on_date": "2026-07-27" +} diff --git a/typescript/tests/services/people.test.ts b/typescript/tests/services/people.test.ts index 42acd4899..536ac6713 100644 --- a/typescript/tests/services/people.test.ts +++ b/typescript/tests/services/people.test.ts @@ -139,4 +139,43 @@ describe("PeopleService", () => { expect(people[0]!.id).toBe(1); }); }); + + describe("outOfOffice", () => { + it("should return out-of-office status including back_on_date", async () => { + server.use( + http.get(`${BASE_URL}/people/1/out_of_office.json`, () => { + return HttpResponse.json({ + person: { id: 1049715913, name: "Victor Cooper" }, + enabled: true, + ongoing: true, + start_date: "2026-07-20", + end_date: "2026-07-26", + back_on_date: "2026-07-27", + }); + }) + ); + + const status = await client.people.outOfOffice(1); + expect(status.enabled).toBe(true); + expect(status.start_date).toBe("2026-07-20"); + expect(status.end_date).toBe("2026-07-26"); + expect(status.back_on_date).toBe("2026-07-27"); + }); + + it("should omit dates when out of office is not enabled", async () => { + server.use( + http.get(`${BASE_URL}/people/1/out_of_office.json`, () => { + return HttpResponse.json({ + person: { id: 1049715913, name: "Victor Cooper" }, + enabled: false, + ongoing: false, + }); + }) + ); + + const status = await client.people.outOfOffice(1); + expect(status.enabled).toBe(false); + expect(status.back_on_date).toBeUndefined(); + }); + }); }); From c8911113595d2a17337238766f4dd6efaa8f90fe Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Wed, 22 Jul 2026 00:41:30 -0700 Subject: [PATCH 08/12] =?UTF-8?q?test:=20appease=20Rails/RefuteMethods=20?= =?UTF-8?q?=E2=80=94=20assert=5Fnot=20over=20refute?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ruby/test/basecamp/services/people_service_test.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ruby/test/basecamp/services/people_service_test.rb b/ruby/test/basecamp/services/people_service_test.rb index 3705d26e7..05e31daa2 100644 --- a/ruby/test/basecamp/services/people_service_test.rb +++ b/ruby/test/basecamp/services/people_service_test.rb @@ -126,7 +126,7 @@ def test_get_out_of_office_disabled_omits_dates result = @account.people.get_out_of_office(person_id: 1) - refute result["enabled"] - refute result.key?("back_on_date") + assert_not result["enabled"] + assert_not result.key?("back_on_date") end end From cd406d88e82d8a2496b88cdd1fa6bca1f362ba74 Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Wed, 22 Jul 2026 00:43:40 -0700 Subject: [PATCH 09/12] test: assert the full disabled-state shape for out-of-office Disabled-state cases now pin ongoing=false and the omission of start_date/end_date alongside back_on_date, in all three suites. --- go/pkg/basecamp/people_test.go | 3 +++ ruby/test/basecamp/services/people_service_test.rb | 3 +++ typescript/tests/services/people.test.ts | 3 +++ 3 files changed, 9 insertions(+) diff --git a/go/pkg/basecamp/people_test.go b/go/pkg/basecamp/people_test.go index cd0f4721c..5d9be893c 100644 --- a/go/pkg/basecamp/people_test.go +++ b/go/pkg/basecamp/people_test.go @@ -483,6 +483,9 @@ func TestOutOfOffice_UnmarshalDisabled(t *testing.T) { if ooo.Enabled { t.Error("expected enabled to be false") } + if ooo.Ongoing { + t.Error("expected ongoing to be false") + } if ooo.StartDate != "" || ooo.EndDate != "" || ooo.BackOnDate != "" { t.Errorf("expected omitted dates to be empty, got start=%q end=%q back_on=%q", ooo.StartDate, ooo.EndDate, ooo.BackOnDate) diff --git a/ruby/test/basecamp/services/people_service_test.rb b/ruby/test/basecamp/services/people_service_test.rb index 05e31daa2..94900ea2d 100644 --- a/ruby/test/basecamp/services/people_service_test.rb +++ b/ruby/test/basecamp/services/people_service_test.rb @@ -127,6 +127,9 @@ def test_get_out_of_office_disabled_omits_dates result = @account.people.get_out_of_office(person_id: 1) assert_not result["enabled"] + assert_not result["ongoing"] + assert_not result.key?("start_date") + assert_not result.key?("end_date") assert_not result.key?("back_on_date") end end diff --git a/typescript/tests/services/people.test.ts b/typescript/tests/services/people.test.ts index 536ac6713..e0af2beff 100644 --- a/typescript/tests/services/people.test.ts +++ b/typescript/tests/services/people.test.ts @@ -175,6 +175,9 @@ describe("PeopleService", () => { const status = await client.people.outOfOffice(1); expect(status.enabled).toBe(false); + expect(status.ongoing).toBe(false); + expect(status.start_date).toBeUndefined(); + expect(status.end_date).toBeUndefined(); expect(status.back_on_date).toBeUndefined(); }); }); From 7ad8125dfb65c1e653d7b4c9eacfdddb452c7209 Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Wed, 22 Jul 2026 00:51:37 -0700 Subject: [PATCH 10/12] spec: model avatar_url on OutOfOfficePerson The pinned doc's out-of-office examples include person.avatar_url (added in this drift window); the embedded person was modeled with only id and name, so typed SDK surfaces dropped it. Added optional avatar_url to the Smithy shape, regenerated, propagated on the Go wrapper, and extended the flow-through tests in Go/TS/Ruby. --- go/pkg/basecamp/people.go | 5 +++-- go/pkg/basecamp/people_test.go | 3 +++ go/pkg/generated/client.gen.go | 5 +++-- openapi.json | 3 +++ python/src/basecamp/generated/types.py | 1 + ruby/lib/basecamp/generated/types.rb | 6 ++++-- ruby/test/basecamp/services/people_service_test.rb | 7 ++++++- spec/basecamp.smithy | 1 + .../Basecamp/Generated/Models/OutOfOfficePerson.swift | 4 +++- typescript/src/generated/openapi-stripped.json | 3 +++ typescript/src/generated/schema.d.ts | 1 + typescript/tests/services/people.test.ts | 7 ++++++- 12 files changed, 37 insertions(+), 9 deletions(-) diff --git a/go/pkg/basecamp/people.go b/go/pkg/basecamp/people.go index 2d35cb169..4cb2ddf68 100644 --- a/go/pkg/basecamp/people.go +++ b/go/pkg/basecamp/people.go @@ -547,8 +547,9 @@ type OutOfOffice struct { // OutOfOfficePerson represents the person associated with an out-of-office status. type OutOfOfficePerson struct { - ID int64 `json:"id"` - Name string `json:"name,omitempty"` + ID int64 `json:"id"` + Name string `json:"name,omitempty"` + AvatarURL string `json:"avatar_url,omitempty"` } // EnableOutOfOfficeRequest specifies the parameters for enabling out-of-office. diff --git a/go/pkg/basecamp/people_test.go b/go/pkg/basecamp/people_test.go index 5d9be893c..0546c38d4 100644 --- a/go/pkg/basecamp/people_test.go +++ b/go/pkg/basecamp/people_test.go @@ -470,6 +470,9 @@ func TestOutOfOffice_Unmarshal(t *testing.T) { if ooo.Person.Name != "Victor Cooper" { t.Errorf("expected person name 'Victor Cooper', got %q", ooo.Person.Name) } + if ooo.Person.AvatarURL == "" { + t.Error("expected non-empty person avatar_url") + } } func TestOutOfOffice_UnmarshalDisabled(t *testing.T) { diff --git a/go/pkg/generated/client.gen.go b/go/pkg/generated/client.gen.go index 26344771d..8fac510d3 100644 --- a/go/pkg/generated/client.gen.go +++ b/go/pkg/generated/client.gen.go @@ -1513,8 +1513,9 @@ type OutOfOfficePayload struct { // OutOfOfficePerson defines model for OutOfOfficePerson. type OutOfOfficePerson struct { - Id int64 `json:"id"` - Name string `json:"name,omitempty"` + AvatarUrl string `json:"avatar_url,omitempty"` + Id int64 `json:"id"` + Name string `json:"name,omitempty"` } // PauseQuestionResponseContent defines model for PauseQuestionResponseContent. diff --git a/openapi.json b/openapi.json index 05321925f..ca0b16249 100644 --- a/openapi.json +++ b/openapi.json @@ -24810,6 +24810,9 @@ "name": { "type": "string", "format": "password" + }, + "avatar_url": { + "type": "string" } }, "required": [ diff --git a/python/src/basecamp/generated/types.py b/python/src/basecamp/generated/types.py index d1313ec3c..17e13ab91 100644 --- a/python/src/basecamp/generated/types.py +++ b/python/src/basecamp/generated/types.py @@ -933,6 +933,7 @@ class OutOfOfficePayload(TypedDict): class OutOfOfficePerson(TypedDict): + avatar_url: NotRequired[str] id: int name: NotRequired[str] diff --git a/ruby/lib/basecamp/generated/types.rb b/ruby/lib/basecamp/generated/types.rb index eef1bd576..d551d1bea 100644 --- a/ruby/lib/basecamp/generated/types.rb +++ b/ruby/lib/basecamp/generated/types.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # Auto-generated from OpenAPI spec. Do not edit manually. -# Generated: 2026-07-22T07:14:48Z +# Generated: 2026-07-22T07:50:03Z require "json" require "time" @@ -2225,7 +2225,7 @@ def to_json(*args) # OutOfOfficePerson class OutOfOfficePerson include TypeHelpers - attr_accessor :id, :name + attr_accessor :id, :avatar_url, :name # @return [Array] def self.required_fields @@ -2234,12 +2234,14 @@ def self.required_fields def initialize(data = {}) @id = parse_integer(data["id"]) + @avatar_url = data["avatar_url"] @name = data["name"] end def to_h { "id" => @id, + "avatar_url" => @avatar_url, "name" => @name, }.compact end diff --git a/ruby/test/basecamp/services/people_service_test.rb b/ruby/test/basecamp/services/people_service_test.rb index 94900ea2d..8e7c51ece 100644 --- a/ruby/test/basecamp/services/people_service_test.rb +++ b/ruby/test/basecamp/services/people_service_test.rb @@ -101,7 +101,11 @@ def test_list_assignable def test_get_out_of_office stub_get("/12345/people/1/out_of_office.json", response_body: { - "person" => { "id" => 1049715913, "name" => "Victor Cooper" }, + "person" => { + "id" => 1049715913, + "name" => "Victor Cooper", + "avatar_url" => "https://example.com/avatar" + }, "enabled" => true, "ongoing" => true, "start_date" => "2026-07-20", @@ -115,6 +119,7 @@ def test_get_out_of_office assert_equal "2026-07-20", result["start_date"] assert_equal "2026-07-26", result["end_date"] assert_equal "2026-07-27", result["back_on_date"] + assert_equal "https://example.com/avatar", result["person"]["avatar_url"] end def test_get_out_of_office_disabled_omits_dates diff --git a/spec/basecamp.smithy b/spec/basecamp.smithy index b996f38d0..227624bb0 100644 --- a/spec/basecamp.smithy +++ b/spec/basecamp.smithy @@ -8419,6 +8419,7 @@ structure OutOfOfficePerson { @required id: PersonId name: PersonName + avatar_url: String } // ============================================================================= diff --git a/swift/Sources/Basecamp/Generated/Models/OutOfOfficePerson.swift b/swift/Sources/Basecamp/Generated/Models/OutOfOfficePerson.swift index 963ab2bd3..53fdfbe5a 100644 --- a/swift/Sources/Basecamp/Generated/Models/OutOfOfficePerson.swift +++ b/swift/Sources/Basecamp/Generated/Models/OutOfOfficePerson.swift @@ -3,10 +3,12 @@ import Foundation public struct OutOfOfficePerson: Codable, Sendable { public let id: Int + public var avatarUrl: String? public var name: String? - public init(id: Int, name: String? = nil) { + public init(id: Int, avatarUrl: String? = nil, name: String? = nil) { self.id = id + self.avatarUrl = avatarUrl self.name = name } } diff --git a/typescript/src/generated/openapi-stripped.json b/typescript/src/generated/openapi-stripped.json index 2e49e4f5e..9e5379bf4 100644 --- a/typescript/src/generated/openapi-stripped.json +++ b/typescript/src/generated/openapi-stripped.json @@ -22486,6 +22486,9 @@ "name": { "type": "string", "format": "password" + }, + "avatar_url": { + "type": "string" } }, "required": [ diff --git a/typescript/src/generated/schema.d.ts b/typescript/src/generated/schema.d.ts index 05900123f..df5d42dd8 100644 --- a/typescript/src/generated/schema.d.ts +++ b/typescript/src/generated/schema.d.ts @@ -3653,6 +3653,7 @@ export interface components { id: number; /** Format: password */ name?: string; + avatar_url?: string; }; PauseQuestionResponseContent: { paused?: boolean; diff --git a/typescript/tests/services/people.test.ts b/typescript/tests/services/people.test.ts index e0af2beff..d2b5f0eee 100644 --- a/typescript/tests/services/people.test.ts +++ b/typescript/tests/services/people.test.ts @@ -145,7 +145,11 @@ describe("PeopleService", () => { server.use( http.get(`${BASE_URL}/people/1/out_of_office.json`, () => { return HttpResponse.json({ - person: { id: 1049715913, name: "Victor Cooper" }, + person: { + id: 1049715913, + name: "Victor Cooper", + avatar_url: "https://example.com/avatar", + }, enabled: true, ongoing: true, start_date: "2026-07-20", @@ -160,6 +164,7 @@ describe("PeopleService", () => { expect(status.start_date).toBe("2026-07-20"); expect(status.end_date).toBe("2026-07-26"); expect(status.back_on_date).toBe("2026-07-27"); + expect(status.person?.avatar_url).toBe("https://example.com/avatar"); }); it("should omit dates when out of office is not enabled", async () => { From 6f92b0ee401b5b3d4589fd10b44035d7f373cfc2 Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Wed, 22 Jul 2026 01:00:56 -0700 Subject: [PATCH 11/12] spec: use the sensitive AvatarUrl shape for OutOfOfficePerson Plain String bypassed the sensitive-type pipeline; Person.avatar_url already targets AvatarUrl (@sensitive). behavior-model.json now redacts $.avatar_url for OutOfOfficePerson and the OpenAPI carries format: password, matching Person. --- behavior-model.json | 3 ++- openapi.json | 3 ++- spec/basecamp.smithy | 2 +- typescript/src/generated/openapi-stripped.json | 3 ++- typescript/src/generated/schema.d.ts | 1 + 5 files changed, 8 insertions(+), 4 deletions(-) diff --git a/behavior-model.json b/behavior-model.json index 7ff08ace4..e126ec6bc 100644 --- a/behavior-model.json +++ b/behavior-model.json @@ -2594,7 +2594,8 @@ "$.avatar_url" ], "OutOfOfficePerson": [ - "$.name" + "$.name", + "$.avatar_url" ], "Person": [ "$.name", diff --git a/openapi.json b/openapi.json index ca0b16249..332c0f13e 100644 --- a/openapi.json +++ b/openapi.json @@ -24812,7 +24812,8 @@ "format": "password" }, "avatar_url": { - "type": "string" + "type": "string", + "format": "password" } }, "required": [ diff --git a/spec/basecamp.smithy b/spec/basecamp.smithy index 227624bb0..b185f0441 100644 --- a/spec/basecamp.smithy +++ b/spec/basecamp.smithy @@ -8419,7 +8419,7 @@ structure OutOfOfficePerson { @required id: PersonId name: PersonName - avatar_url: String + avatar_url: AvatarUrl } // ============================================================================= diff --git a/typescript/src/generated/openapi-stripped.json b/typescript/src/generated/openapi-stripped.json index 9e5379bf4..d652d76a9 100644 --- a/typescript/src/generated/openapi-stripped.json +++ b/typescript/src/generated/openapi-stripped.json @@ -22488,7 +22488,8 @@ "format": "password" }, "avatar_url": { - "type": "string" + "type": "string", + "format": "password" } }, "required": [ diff --git a/typescript/src/generated/schema.d.ts b/typescript/src/generated/schema.d.ts index df5d42dd8..387afa9c6 100644 --- a/typescript/src/generated/schema.d.ts +++ b/typescript/src/generated/schema.d.ts @@ -3653,6 +3653,7 @@ export interface components { id: number; /** Format: password */ name?: string; + /** Format: password */ avatar_url?: string; }; PauseQuestionResponseContent: { From bd40ab987c2893c399370275aa1e90aac8982ed6 Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Wed, 22 Jul 2026 01:09:39 -0700 Subject: [PATCH 12/12] test: assert ongoing in the enabled out-of-office cases too --- ruby/test/basecamp/services/people_service_test.rb | 1 + typescript/tests/services/people.test.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/ruby/test/basecamp/services/people_service_test.rb b/ruby/test/basecamp/services/people_service_test.rb index 8e7c51ece..50ff663f9 100644 --- a/ruby/test/basecamp/services/people_service_test.rb +++ b/ruby/test/basecamp/services/people_service_test.rb @@ -116,6 +116,7 @@ def test_get_out_of_office result = @account.people.get_out_of_office(person_id: 1) assert result["enabled"] + assert result["ongoing"] assert_equal "2026-07-20", result["start_date"] assert_equal "2026-07-26", result["end_date"] assert_equal "2026-07-27", result["back_on_date"] diff --git a/typescript/tests/services/people.test.ts b/typescript/tests/services/people.test.ts index d2b5f0eee..173b89915 100644 --- a/typescript/tests/services/people.test.ts +++ b/typescript/tests/services/people.test.ts @@ -161,6 +161,7 @@ describe("PeopleService", () => { const status = await client.people.outOfOffice(1); expect(status.enabled).toBe(true); + expect(status.ongoing).toBe(true); expect(status.start_date).toBe("2026-07-20"); expect(status.end_date).toBe("2026-07-26"); expect(status.back_on_date).toBe("2026-07-27");