Correct column move, question update, lineup marker, project timesheet and event boost contracts
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.rbvalid_destinationrequiresdestination.kanban_board?.Kanban::MovesController#createrescuesActiveModel::ValidationErrorto 422.Recording::Positioner#rescoretreats position<= 1as the top and> countas the bottom.doc/api/sections/card_table_columns.mdsays "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_idasCardColumnId. 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 onsource_id,target_idandposition. GoMoveColumnRequestdocs are corrected, andCardColumnsService.Movefills an unset (zero)TargetIDwith 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.rbquestion_paramspermits only:titleandschedule: [...].doc/api/sections/questions.md"Update a question" says thequestionobject "must includetitle,schedule, or both". - What was wrong: a paused-only update answered 400.
pausedsent with a title was dropped silently and the call reported success, so the question stayed unpaused. Pausing has its own operations (PauseQuestion/ResumeQuestion). - Fix:
pausedis removed fromUpdateQuestionInput, and the doc points to pause/resume. GoUpdateQuestionnow 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.rbcreaterenders@marker.as_json(except: :account_id), status: :created, andupdaterenders 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. GoCreateMarkerandUpdateMarkerreturn(*LineupMarker, error). - bc3 doc follow-up:
doc/api/sections/lineup_markers.mdstill says "201 Createdwith an empty response body" and "200 OKwith an empty response body". Those lines are stale.
D. Project gains timesheet_enabled
- bc3:
doc/api/sections/projects.mdresponse examples includetimesheet_enabled.TimesheetsController#set_recordingraisesRecordNotFoundunlessproject.timesheet_recordingexists, andProject::Timesheetable#record_timesheetcreates it only once the project hastimesheet_enabled. - What was wrong: the SDK dropped the field, so a caller couldn't tell why
GET /projects/:id/timesheet.jsonanswered 404. - Fix:
Project.timesheet_enabledis added, with a doc comment about the 404. Go:Project.TimesheetEnabled bool. - Possible follow-up, not done here: bc3
ProjectOperations#create_project_paramspermitstimesheet_enabledon project create and update, but only for people withcan_access_timesheet?.doc/apidoesn'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:23BOOSTABLE_ACTIONS = %w[ completed adopted column_changed moved_in ].BoostsController#ensure_boostableanswers 403 on index and create for any other event.#ensure_comments_are_openanswers 403 on create when the recording's comments are closed.#set_boostableanswers 404 (render_not_found) for a recording the person can't see. - Fix: doc comments on
ListEventBoosts,CreateEventBoostandEvent.boosts_count/boosts_url. The fields were already in the spec. This change is docs only. - bc3 doc follow-up:
doc/api/sections/boosts.mdlistscompleted,adoptedandcolumn_changedbut leaves outmoved_in.
Breaking changes (in MIGRATING.md under v0.25.0)
- Go:
UpdateQuestionRequest.Pausedis removed. Every SDK's update-question request type losespaused. - Go:
LineupService.CreateMarkerandUpdateMarkernow return(*LineupMarker, error)instead oferror. 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 theTimesheetEnabledassertion againstspec/fixtures/projects/get.json. make generatepasses (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.rbandtypescript/src/generated/metadata.ts, resolved by rerunningmake generateat each commit. All 62make checktargets 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_idis now the card table, not the column; Basecamp rejects any other destination with 422, andpositionis the 1-based slot, with negative values refused. Go'sMovefills an unset target with the card table it is called with.UpdateQuestiondropspaused, 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.
Projectgainstimesheet_enabledso 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
pausedis removed from every SDK's update-question request type; Go'sUpdateQuestionRequest.Pausedis gone.- Go
LineupService.CreateMarker/UpdateMarkerreturn(*LineupMarker, error); other SDKs return the marker where they returned nothing. - Migration steps are recorded in
MIGRATING.mdunder v0.25.0.