Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
50 changes: 34 additions & 16 deletions COORDINATION.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
# Coordination: SDK ↔ BC3 API Parity for BC5

This SDK works in lockstep with the BC3 API parity plan, tracked on the
`basecamp/bc3` `five+api` branch. The authoritative server-side audit lives
in that branch under `tmp/api_audit.md` (generated by the BC3 plan's
This SDK works in lockstep with the BC3 API parity plan. With the BC5 API
train merged (8 PRs to `basecamp/bc3` `master`, 2026-07-18..21), the contract
authority is **`doc/api/` on `master`** — doc examples are regenerated from
Comment thread
jeremy marked this conversation as resolved.
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`).

## Division of labor
Expand All @@ -29,25 +31,41 @@ in that branch under `tmp/api_audit.md` (generated by the BC3 plan's

## Contract decisions (cross-team)

Two items the SDK canary surfaced that need product/API leadership decisions:
Two items the SDK canary surfaced. As of the BC5 API train (8 BC3 PRs merged
to `master`, 2026-07-18..21) both are **settled**:

1. `memories` going to `[]` on `GET /my/readings.json` — back-compat or
accept-and-document. BC4 populates the array; BC5 emits literal `[]`.
Existing BC4 API users reading `memories` would silently see no data on
BC5 — a subtractive change disguised as compatible.
1. `memories` going to `[]` on `GET /my/readings.json` — **settled:
permanently empty by documented contract**.
`doc/api/sections/my_notifications.md` (language codified by BC3 #11628)
documents `memories` as an always-empty placeholder replaced by
`bubble_ups`. BC3 #10947, which would have repopulated it via
`json.memories @bubble_ups`, **closed unmerged** — superseded by the train;
the alias never shipped and never will. Filed as
[`spec/api-gaps/memories-emptied-regression.md`](spec/api-gaps/memories-emptied-regression.md)
(status `addressed-in-bc3-pr-11628`). PR #308's live canary encodes the
additive-only invariant
(`pairwiseSupersetArray: ["memories"]` on `GetMyNotifications`) with a
`pairwiseDeltaAllowed: ["memories"]` waiver that is now **permanent** —
the machine-readable record of the accepted BC4→BC5 delta; that canary
surface lives on #308's branch (`conformance/tests/live-my-surface.json`),
not here. Retire the waiver only if BC4 empties `memories` or BC5's
documented contract changes. (There is no commit `64acf34` — the
once-assumed alias is counterfactual.)
2. Longstanding `app_todoslists_url` typo at
`app/views/api/todosets/_todoset.json.jbuilder:28` — server fix, alias
period, or SDK accommodation. The SDK spec models the correctly-spelled
`app_todolists_url`, so SDK clients reading that field have always seen
`null`.
`app/views/api/todosets/_todoset.json.jbuilder:28` — unchanged by the
train: post-train `master` still carries **only the typo**, no
correctly-spelled alias (re-verified 2026-07-21). Impact is nil for SDK
consumers — the spec models the correctly-spelled `app_todolists_url`,
which has always read `null`, so there is no regression either way. No SDK
action needed.

Both are tracked in the BC3 plan's Phase 2 compat verification and this
SDK plan's §8.
Both records live on in the registry and this SDK plan's §8.

## Where to look

- BC3-side authoritative inventory: `tmp/api_audit.md` (generated by BC3
plan's `script/api/audit`)
- BC3-side contract authority: `doc/api/` on `basecamp/bc3` `master`
(post-train; the historical `tmp/api_audit.md` inventory lived on
`five+api`)
- SDK-side absorption status: [`spec/api-gaps/README.md`](spec/api-gaps/README.md)
- Live canary results: `make check-bc5-compat` against current refs
(lands in PR 4 of the SDK plan)
Expand Down
15 changes: 7 additions & 8 deletions go/pkg/generated/client.gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -23585,7 +23585,7 @@
"items": {
"$ref": "#/components/schemas/Notification"
},
"description": "Legacy \"save forever\" collection. Observed BC5 behavior: emits `[]`\nwhile BC4 still populates with real items — the BC team has not yet\nresolved whether to keep BC4-shaped data on BC5 (back-compat) or to\naccept the empty-array break with a documented BC5 changelog entry.\nSee COORDINATION.md for the open decision. The conceptual\nreplacement is `bubble_ups` (with optional scheduling via\n`scheduled_bubble_ups`), though wire shapes are not interchangeable\nper-item, so cross-version readers should consume both."
"description": "Legacy \"save forever\" collection. Permanently `[]` on BC5 by documented\ncontract (`doc/api/sections/my_notifications.md`, codified by BC3 #11628):\nan always-empty placeholder superseded by `bubble_ups`. BC4 (the `four`\nbranch) still populates it — an accepted BC4→BC5 subtractive delta\nrecorded in `spec/api-gaps/memories-emptied-regression.md`. New\nintegrations should use `bubble_ups` / `scheduled_bubble_ups` and must\nnot rely on `memories` on BC5."
},
"bubble_ups": {
"type": "array",
Expand Down
2 changes: 1 addition & 1 deletion ruby/lib/basecamp/generated/metadata.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://basecamp.com/schemas/sdk-metadata.json",
"version": "1.0.0",
"generated": "2026-07-22T02:19:46Z",
"generated": "2026-07-22T02:53:58Z",
"operations": {
"GetAccount": {
"retry": {
Expand Down
2 changes: 1 addition & 1 deletion ruby/lib/basecamp/generated/types.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

# Auto-generated from OpenAPI spec. Do not edit manually.
# Generated: 2026-07-22T02:19:46Z
# Generated: 2026-07-22T02:53:58Z

require "json"
require "time"
Expand Down
25 changes: 18 additions & 7 deletions spec/api-gaps/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,28 @@ making the absorption journey publicly auditable.

| Gap | Status | BC3 plan phase | SDK demand |
|---|---|---|---|
| [calendar](calendar.md) | no-json-contract | 3b | medium |
| [scratchpad](scratchpad.md) | no-json-contract | 3b | medium |
| [step-top-level](step-top-level.md) | partial-coverage | 3b | low |
| [everything-aggregates](everything-aggregates.md) | no-json-contract | 3c | high |
| [activity-timeline](activity-timeline.md) | no-json-contract | 3d | high |
| [calendar](calendar.md) | addressed-in-bc3-pr-12321 | 3b | medium |
| [scratchpad](scratchpad.md) | addressed-in-bc3-pr-12322 | 3b | medium |
| [step-top-level](step-top-level.md) | absorbed-in-sdk | 3b | low |
| [everything-aggregates](everything-aggregates.md) | addressed-in-bc3-pr-11627 | 3c | high |
| [activity-timeline](activity-timeline.md) | addressed-in-bc3-pr-11629 | 3d | high |
| [recordable-subtypes-doc](recordable-subtypes-doc.md) | partial-coverage | 3a | medium |
| [stack-doc-and-smithy](stack-doc-and-smithy.md) | partial-coverage | 3b | medium |
| [search-filter-additions](search-filter-additions.md) | no-json-contract | 3e | medium |
| [stack-doc-and-smithy](stack-doc-and-smithy.md) | confirmed-not-api-resource | 3b | medium |
| [search-filter-additions](search-filter-additions.md) | partial-coverage | 3e | medium |
| [rich-text-project-attachable](rich-text-project-attachable.md) | no-json-contract | 3e | low |
| [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 |

> 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
> 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.
> `stack-doc-and-smithy` is retained as a `confirmed-not-api-resource`
> classification record (Stacks — renamed Folders in the product — are
> web-only on both `four` and `master`).

The detector also maintains [`allowlist.yml`](allowlist.yml) for routes
classified as not-an-API-resource or absorbed under another entry. Allowlist
Expand Down
105 changes: 66 additions & 39 deletions spec/api-gaps/activity-timeline.md
Original file line number Diff line number Diff line change
@@ -1,70 +1,97 @@
---
gap: activity-timeline
status: no-json-contract
status: addressed-in-bc3-pr-11629
detected: 2026-05-01
sdk_demand: high
bc3_pr: 11629
bc3_refs:
introduced_in: five
bc3_plan_phase: 3d
routes:
- GET /:account_id/activity.json
- GET /:account_id/reports/progress.json
- GET /:account_id/projects/:project_id/timeline.json
- GET /:account_id/reports/users/progress/:person_id.json
Comment thread
jeremy marked this conversation as resolved.
controllers:
- app/controllers/activity_controller.rb
- app/controllers/timelines_controller.rb
related_existing_api: []
- app/controllers/users/timelines_controller.rb
related_existing_api:
- GetProgressReport
- GetProjectTimeline
- GetPersonProgress
---

# Activity Timeline (global + per-project)
# Activity Timeline (account, project, and person)

## What's missing

BC5 introduces a global activity feed (`/activity.json`) and per-project
timeline (`/projects/:project_id/timeline.json`) backed by an Activity
envelope. The BC3 plan Phase 3d ships both routes plus the envelope shape.
Additive fields only — the routes and base contract are documented and already
modeled. The merged `doc/api/sections/timeline.md` on `master` documents
exactly three routes:

Note: there is **no `/buckets/:id/timeline` route** — earlier drafts of this
brief used the wrong path. Per-project routes are keyed by `project_id`, not
`bucket_id`.
- **Account** — `GET /reports/progress.json`: a paginated bare array of
timeline events across all projects the authenticated user can access.
- **Project** — `GET /projects/:project_id/timeline.json`: the same event
shape, pre-filtered to one project, also a bare array.
- **Person** — `GET /reports/users/progress/:person_id.json`: a JSON
**object** `{person, events}` — the person plus a paginated `events` list of
timeline events they created.
Comment thread
jeremy marked this conversation as resolved.

These routes **predate the BC5 API train**: they are a BC4-era contract,
documented since the #9981 docs repatriation, that BC5 kept. The train PR that
re-verified and regenerated `timeline.md` against live BC5 is BC3 **#11629**
(the doc-generation tooling PR), which is what `addressed-in-bc3-pr-11629`
records here.

Historical corrections retained from earlier drafts: there is no
`/activity.json` route and no `/buckets/:id/timeline` route; the BC5-new
`/activity/days/:date` and `/activity/dates` sub-routes were removed in the
timeline rewrite and were never modeled here.

## Why it matters

Activity feeds are a primary integration surface for dashboards, audit logs,
and "what's new since I last checked" tooling. Without a JSON API, consumers
have to poll individual recording endpoints and reconstruct ordering — slow,
incomplete, and incompatible with the in-app activity stream.
and "what's new since I last checked" tooling. The SDK already models all
three routes (`GetProgressReport`, `GetProjectTimeline`, `GetPersonProgress`
at `spec/basecamp.smithy:7084`, `:7105`, `:7130`, including the person-route
`{person, events}` object wrapper), so consumers can call them today — but the
event payload's typed surface lags the merged doc, leaving fields to be
consumed untyped.

## Suggested API shape

`GET /:account_id/activity.json`:
- Pagination: Link header, `X-Total-Count`.
- Response: array of `Activity` envelope objects:
- `id` (long), `created_at`, `updated_at`
- `kind` (string — what happened)
- `recording` (polymorphic: Todo, Card, Message, etc.)
- `creator` (Person)
- `bucket` (TodoBucket-shaped, when scoped)
The remaining absorption is additive fields on the event shape, per the merged
`doc/api/sections/timeline.md`:

`GET /:account_id/projects/:project_id/timeline.json`:
- Same envelope, pre-filtered to the project.
- Pagination: same as global.
- `kind` — a 15-value vocabulary: `message_created`, `comment_created`,
`todo_created`, `todo_completed`, `upload_created`, `document_created`,
`schedule_entry_created`, `schedule_entry_rescheduled`, `question_created`,
`question_answer_created`, `chat_transcript_rollup`, `kanban_card_created`,
`kanban_card_completed`, `inbox_forward_created`,
`client_correspondence_created`.
- `data` — event-specific payload; for `schedule_entry_created` /
`schedule_entry_rescheduled` it carries `{all_day, starts_at, ends_at}`.
- `avatars_sample` — array of avatar URLs (used by chat rollups to show
participants).
- `attachments` — array of attached files, if any.
- Plus the documented envelope fields (`parent_recording_id`, `action`,
`target`, `title`, `summary_excerpt`, `bucket`, `creator`, `url`,
`app_url`).

## Implementation notes for BC3

- Activity envelope likely already exists in some form (used by web UI). BC3
Phase 3d work makes it a documented, JSON-serialisable contract.
- Per-project timeline action lives on `timelines_controller.rb`, not on
`projects_controller`.
- `doc/api/sections/activity.md` covers both the global and per-project
routes plus the envelope shape.
Shipped — nothing pending. The account and project routes serve from
`timelines_controller.rb` and the person route from
`users/timelines_controller.rb`; `doc/api/sections/timeline.md` is regenerated
against live BC5 by the doc tooling from #11629.

## SDK absorption plan when this lands

- New `ActivityService` with `getActivity()` (global) and
`getProjectTimeline(projectId)` (per-project).
- New shape: `Activity` envelope.
- The recording field is polymorphic — model as a union of existing recording
shapes if Smithy supports it, otherwise use a discriminated structure with
a `type` field.
- Canary fixture: include `getActivity()` once schema is stable. Pairwise
check is BC4-absent → BC5-present.
- No new operations — `GetProgressReport`, `GetProjectTimeline`, and
`GetPersonProgress` already exist with the correct paths and the
person-route object wrapper.
- Extend the timeline event shape with the additive fields above: the `kind`
vocabulary (model as an open string enum — BC3 says "common values
include", so treat it as non-exhaustive), the `data` struct for
schedule-entry events, `avatars_sample`, and `attachments`.
- Canary fixture: `GetProgressReport` exercises the account feed; pairwise
check is structural (the routes exist on both BC4 and BC5).
56 changes: 28 additions & 28 deletions spec/api-gaps/calendar.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
---
gap: calendar
status: no-json-contract
status: addressed-in-bc3-pr-12321
detected: 2026-05-01
sdk_demand: medium
bc3_pr: 12321
bc3_refs:
introduced_in: five
bc3_plan_phase: 3b
Expand All @@ -18,46 +19,45 @@ bc3_refs:

## What's missing

BC5 introduces a top-level Calendar resource (a per-user calendar view, not the
per-project Schedule). The web app reads and updates calendar settings via
`calendars_controller.rb`, but there are no JSON-formatted responses on those
routes today. The BC3 parity plan Phase 3b ships **show + update only** —
not full CRUD.
SDK absorption only — the contract shipped via BC3 **#12321** in the BC5 API
train (2026-07-18..21). `doc/api/sections/calendars.md` on `master` is the
contract of record, documenting the planned **show + update only** scope
(not full CRUD):

- `GET /calendars/:id.json` — returns the calendar (keyed by bucket id).
- `PUT /calendars/:id.json` — updates the calendar.

The Calendar is a top-level BC5 resource (a calendar view distinct from the
per-project Schedule) with no BC4 analog, so additive coverage is safe.

## Why it matters

Without `GET` an SDK client can't display the user's calendar surface in a
custom integration. Without `PUT` consumers can't set the few mutable
properties (e.g. visibility, default view) the web UI exposes. This is a new
top-level resource on BC5 with no BC4 analog, so additive coverage is safe.
custom integration. Without `PUT` consumers can't set the mutable properties
the web UI exposes. This is a new top-level resource on BC5 with no BC4
analog, so additive coverage is safe.

## Suggested API shape

`GET /:account_id/calendars/:id.json`:
- `id` (long), `name` (string), `url`, `app_url`
- `view` ("day" | "week" | "month" | ...) and any other settings the form exposes
- `creator` (Person), `created_at`, `updated_at`
- `subscribed_buckets` (array of Bucket-shaped) if applicable

`PUT /:account_id/calendars/:id.json`:
- Accept the mutable subset of the show payload
- Return `204 No Content` on success or the updated resource
Per the merged `doc/api/sections/calendars.md` — derive the exact field list
from the doc's captured examples at absorption time rather than restating it
here (the doc examples are regenerated from live BC5 by the #11629 tooling).

## Implementation notes for BC3

- New `app/views/api/calendars/_calendar.json.jbuilder` partial; `show.json.jbuilder`,
`update.json.jbuilder` reuse it.
- Add `respond_to :json` (or per-action `format.json`) to the relevant actions
in `calendars_controller.rb`.
- Add `doc/api/sections/calendars.md` describing both routes.
- Re-evaluate `index/create/destroy` if usage signals demand later — initial
scope is intentionally small.
Shipped — nothing pending. `calendars_controller.rb` serves both routes with
JSON branches; `doc/api/sections/calendars.md` documents them. Re-evaluate
`index/create/destroy` only if usage signals demand later — the shipped scope
is intentionally small.

## SDK absorption plan when this lands

- New Smithy operations: `GetCalendar`, `UpdateCalendar`.
- New shapes: `Calendar`, `UpdateCalendarInput`/`Output`.
- New service registration: `CalendarsService` with `get(id)` and `update(id, input)`.
- New Smithy operations: `GetCalendar`, `UpdateCalendar`, with shapes derived
from the merged doc's examples.
- New service registration: `CalendarsService` with `get(id)` and
`update(id, input)`.
- Status flips to `absorbed-in-sdk` with the absorption PR (which adds the
Smithy refs).
- Canary fixture: add `GetCalendar` to `live-my-surface.json` once a stable
fixture-id resolution path exists (e.g., dock walk → calendar tool).
- Pairwise check: BC4 absent → BC5 present is fine; the assertion is structural
Expand Down
Loading
Loading