Correct column move, question update, lineup marker, project timesheet and event boost contracts

Merged
JE
2d ago
+655-303

Five SDK contracts that disagree with what bc3 actually does. A production every-tool pass on the Basecamp MCP server (v0.2.7) found them. Each cause is in this SDK's spec or Go wrapper, so each fix belongs here. The MCP server only documents what the SDK sends and returns. All cited bc3 behavior is inside the revision spec/api-provenance.json currently pins, and the cited code is unchanged on bc3 HEAD, so no repin.

Changes

A. MoveCardColumn: target_id is the card table, position is a 1-based slot

  • bc3: app/models/kanban/moves/column_move.rb valid_destination requires destination.kanban_board?. Kanban::MovesController#create rescues ActiveModel::ValidationError to 422. Recording::Positioner#rescore treats position <= 1 as the top and > count as the bottom. doc/api/sections/card_table_columns.md says "target_id - the id of the card table" and "position - index among the columns (ignoring Triage, Not Now or Done). Default: 1".
  • What was wrong: the spec typed target_id as CardColumnId. The Go doc said "the column ID to move relative to" and "zero-indexed position". A caller who followed the docs got 422 on every call; the MCP tool built on them failed every time.
  • Fix: target_id: CardTableId, plus doc comments on source_id, target_id and position. Go MoveColumnRequest docs are corrected, and CardColumnsService.Move fills an unset (zero) TargetID with the card table it is called with. The TS and Ruby tests now send the card table as the target.

B. UpdateQuestion: drop paused

  • bc3: app/controllers/questions_controller.rb question_params permits only :title and schedule: [...]. doc/api/sections/questions.md "Update a question" says the question object "must include title, schedule, or both".
  • What was wrong: a paused-only update answered 400. paused sent with a title was dropped silently and the call reported success, so the question stayed unpaused. Pausing has its own operations (PauseQuestion/ResumeQuestion).
  • Fix: paused is removed from UpdateQuestionInput, and the doc points to pause/resume. Go UpdateQuestion now refuses an update with neither a title nor a schedule before it reaches the wire (ErrUsage).

C. Create/UpdateLineupMarker return the marker

  • bc3: app/controllers/lineup/markers_controller.rb create renders @marker.as_json(except: :account_id), status: :created, and update renders the same with :ok. This has been true since bc3 f85369fbad4 (#9946, 2026-03-18).
  • What was wrong: the outputs were empty, so the SDKs threw the marker away and callers had to list markers to find the new id.
  • Fix: both outputs carry LineupMarker. Go CreateMarker and UpdateMarker return (*LineupMarker, error).
  • bc3 doc follow-up: doc/api/sections/lineup_markers.md still says "201 Created with an empty response body" and "200 OK with an empty response body". Those lines are stale.

D. Project gains timesheet_enabled

  • bc3: doc/api/sections/projects.md response examples include timesheet_enabled. TimesheetsController#set_recording raises RecordNotFound unless project.timesheet_recording exists, and Project::Timesheetable#record_timesheet creates it only once the project has timesheet_enabled.
  • What was wrong: the SDK dropped the field, so a caller couldn't tell why GET /projects/:id/timesheet.json answered 404.
  • Fix: Project.timesheet_enabled is added, with a doc comment about the 404. Go: Project.TimesheetEnabled bool.
  • Possible follow-up, not done here: bc3 ProjectOperations#create_project_params permits timesheet_enabled on project create and update, but only for people with can_access_timesheet?. doc/api doesn't document it as a write parameter, so this PR doesn't add it to the inputs.

E. Event boosts: which events are boostable

  • bc3: app/models/event.rb:23 BOOSTABLE_ACTIONS = %w[ completed adopted column_changed moved_in ]. BoostsController#ensure_boostable answers 403 on index and create for any other event. #ensure_comments_are_open answers 403 on create when the recording's comments are closed. #set_boostable answers 404 (render_not_found) for a recording the person can't see.
  • Fix: doc comments on ListEventBoosts, CreateEventBoost and Event.boosts_count/boosts_url. The fields were already in the spec. This change is docs only.
  • bc3 doc follow-up: doc/api/sections/boosts.md lists completed, adopted and column_changed but leaves out moved_in.

Breaking changes (in MIGRATING.md under v0.25.0)

  • Go: UpdateQuestionRequest.Paused is removed. Every SDK's update-question request type loses paused.
  • Go: LineupService.CreateMarker and UpdateMarker now return (*LineupMarker, error) instead of error. The other SDKs return the marker where they returned nothing.

Verification

  • Go tests were written first and failed before the fix: TestCardColumnsService_MoveTargetsTheCardTable, TestCheckinsService_UpdateQuestionRequiresTitleOrSchedule, TestLineupService_MarkerWritesReturnTheMarker, TestProjectFromGenerated_TimesheetEnabled, plus the TimesheetEnabled assertion against spec/fixtures/projects/get.json.
  • make generate passes (exit 0).
  • Rebased onto main after #989, #988 and #990 merged. The only conflicts were generation timestamps in ruby/lib/basecamp/generated/metadata.json, types.rb and typescript/src/generated/metadata.ts, resolved by rerunning make generate at each commit. All 62 make check targets pass at 425d18c.

Overlap

#990 and #988 have merged; this PR is rebased on top of both, and the combined ListMarkers keeps #990's empty-not-nil initialization.


Summary by cubic

Corrects five SDK contracts that disagreed with what Basecamp actually does, found in a production pass on the MCP server. Each fix brings the spec, generated SDKs, and Go wrapper in line with bc3 behavior.

Contract fixes

  • MoveCardColumn.target_id is now the card table, not the column; Basecamp rejects any other destination with 422, and position is the 1-based slot, with negative values refused. Go's Move fills an unset target with the card table it is called with.
  • UpdateQuestion drops paused, which Basecamp never reads on update; Go refuses an update with neither title nor schedule before the wire, and a schedule replaces the whole schedule.
  • Lineup marker create and update now return the saved marker instead of nothing.
  • Project gains timesheet_enabled so callers can tell why the timesheet report answers 404.
  • Event boost operations document which events are boostable and their 403/404 cases.
  • Because the TS, Ruby and Kotlin generators drop member docs, contract details — the card-table target, the 1-based position, the whole-schedule replacement — now live in operation descriptions, kept single-line so those generators don't truncate them.

Breaking changes

  • paused is removed from every SDK's update-question request type; Go's UpdateQuestionRequest.Paused is gone.
  • Go LineupService.CreateMarker/UpdateMarker return (*LineupMarker, error); other SDKs return the marker where they returned nothing.
  • Migration steps are recorded in MIGRATING.md under v0.25.0.