Correct the model where Fizzy's controllers disagree with it
Corrects spec/fizzy.smithy for the tools the controller sweep on basecamp/fizzy-mcp-server#9 found disagreeing with Fizzy at 27d0d2e. The flat catalog in #9 is generated from this model, so the fix belongs here rather than in a growing flat.Corrections table. I checked every row against the controller, model or concern at that commit before changing the spec. Everything else is regenerated: openapi.json, behavior-model.json, URL routes, and all six SDKs (Go, TypeScript, Ruby, Kotlin, Swift, Rust).
Per tool
The sweep table names 30 tools by name. "Entropy periods" adds create_board, which is 31. I covered all of them. Where the table's "..." pointed at more list_cards parameters, I documented those too.
| Tool | Sweep finding | Change |
|---|---|---|
list_notifications | ignores read | @httpQuery("read") removed. The operation doc says it takes no filter: up to 100 unread, then a page of read (NotificationsController#index) |
create_card, update_card | image needs an upload | Documented, not removed. image is the signed blob id from CreateDirectUpload, which the SDK has and fizzy-cli uses (card create/update --image). Removing it would break a working SDK path. Only MCP can't upload, and it already excludes CreateDirectUpload |
assign_card, self_assign_card, tag_card | toggles | Operation docs say they toggle, that both outcomes return 204, and that a repeat reverses the first call. tag_card also creates the tag when it's missing |
postpone_card, triage_card, un_triage_card | not idempotent: each call records an event | @fizzyIdempotent removed, and @idempotent too on the DELETE. Retry is maxAttempts: 1. The doc says each call notifies webhooks and adds a system comment |
unpin_card | not idempotent: a retry hits nil.destroy | @idempotent and @fizzyIdempotent removed, maxAttempts: 1. The doc records the 500 on a card that isn't pinned (I inferred that from unpin_by and didn't run it) |
update_board_involvement | involvement required | @required. Values access_only or watching documented |
update_user | name required | @required |
update_account_settings | name required | @required |
update_account_entropy, update_board_entropy | period required, values unchecked | @required. Values 3, 7, 11, 30, 90 or 365 documented (Entropy::AUTO_POSTPONE_PERIODS_IN_DAYS; 11 really is in the list) |
create_board, update_board | entropy periods | Same values documented on auto_postpone_period_in_days |
update_join_code | usage_limit required | @required. The doc notes that Fizzy accepts up to 10,000,000,000 but the field is 32-bit. Widening it to Long would break fizzy-cli's int32 assignment, so it isn't done here |
update_notification_settings | bundle_email_frequency required | @required. Values never, every_few_hours, daily or weekly documented |
update_user_role | any other role demotes to member | Values member or admin documented, along with the fact that anything else, owner included, sets member |
create_webhook, update_webhook | unknown actions dropped | The 11 Webhook::PERMITTED_ACTIONS documented. Also noted: unknown actions drop silently, a webhook with none gets no deliveries, and an update replaces the list |
create_column, update_column | color values | The nine Color::COLORS CSS values documented. Any other value is stored but reads back as Blue |
list_cards | indexed_by, sorted_by, ... | Values and fallbacks documented for indexed_by, sorted_by, assignment_status, creation and closure |
mark_card_read, mark_card_unread | succeed without a notification | No-op documented |
move_column_left, move_column_right | no-op at the edge | No-op documented |
update_my_timezone | unknown zone accepted | Documented: it's stored, returns 204, and reads back as UTC |
update_board | "at least one field", 400 on user_ids alone, user_ids replaces membership | All three documented |
update_card, update_column, update_step, update_webhook | "at least one field" | Documented (Rails param wrapping makes an empty body {key: {}}, which params.expect rejects with 400) |
Not spec changes: these belong in Fizzy
These three are documented here as current behavior, but the fix is a Fizzy change:
update_boardwith onlyuser_idsreturns 400, becauseBoardsController#board_paramsexpects aboardobject thatuser_idsisn't wrapped into.unpin_cardon a card that isn't pinned returns 500 (pins.find_by(...).tap { it.destroy }).- Toggle-on-create:
POST .../assignments,.../self_assignmentand.../taggingstoggle. An idempotent add and remove would need new endpoints or a parameter in Fizzy.
The earlier flat.Corrections entries
Before the sweep, #9 had already hand-corrected 11 operations. Each one was checked against a controller, so I fixed them here too. Without that, a release and resync would retire none of flat.Corrections:
CreateCard:board_idis@required.column_id,assignee_idsandtag_namesare removed, becausecard_paramspermits none of them.UpdateCard:column_idremoved.MoveCard:column_idremoved.UpdateWebhook:urlremoved (webhook_params.except(:url)), with the reason in the operation doc.TriageCard:column_idis@required.MoveColumnLeft,MoveColumnRightandResetJoinCodeare no longer idempotent, and retry ismaxAttempts: 1.
I left three unfixed in the model, because the fix changes a generated method signature that fizzy-cli calls:
ListWebhooksandListBoardAccessesare paged by Fizzy. Adding@fizzyPaginationchanges the Go service toList(ctx, boardID, path)andListBoardAccesses(ctx, boardID, path), and fizzy-cli calls both with the old arguments.ListWebhooksnow documents that Fizzy pages it.BulkReadNotifications: removing the ignorednotification_idschanges GoBulkRead(ctx, req)toBulkRead(ctx), and fizzy-cli callsBulkRead(ctx, nil). The field is now documented as ignored.
After this is released and resynced into fizzy-mcp-server, 8 of the 11 flat.Corrections entries stop changing anything, so Load fails until each one is deleted. Those are CreateCard, UpdateCard, MoveCard, MoveColumnLeft, MoveColumnRight, ResetJoinCode, UpdateWebhook and TriageCard. That leaves ListWebhooks, ListBoardAccesses and BulkReadNotifications. Two notes for that resync:
- The flat package builds tool descriptions without the operation description (
describe()). The new operation docs (toggles, no-ops, at-least-one-field,UpdateWebhook's URL note) only reach tool descriptions oncedescribe()appendsop.Description. Member docs already reach the input schemas. - None of the sweep rows ever became corrections, so for them the resync just adds information.
Enums: documented values, not enum shapes
I tried real Smithy enum/intEnum shapes first. oapi-codegen turns them into named Go types (type AssignableUserRole string, type AutoPostponePeriod int32) plus package-level constants (Admin, Member, N3, ...). fizzy-cli assigns plain variables to these fields (Role: userRoleRole, Involvement: ..., BundleEmailFrequency: ..., SubscribedActions: []string, AutoPostponePeriodInDays: int32(...)), so it would stop compiling. TypeScript would turn them into literal unions, with the same break for string variables.
So I kept every type as it was and listed the accepted values, and what happens to any other value, in the member's @documentation. That text reaches OpenAPI description, the generated doc comments in every language, and the MCP input schemas. Moving to real enums is a breaking change for fizzy-cli and is left for a separate decision.
Breaking changes for SDK consumers
This is 0.x, but every change is listed here. Each one is a call that already failed or did nothing at runtime.
- Removed request members:
CreateCard'scolumn_id,assignee_idsandtag_names;UpdateCard's andMoveCard'scolumn_id;UpdateWebhook'surl;ListNotifications'sread. Fizzy ignores all of them, so a caller that set one gets a compile error instead of a silent no-op. In Go,generated.ListNotificationsParamsis gone. In Kotlin,ListNotificationsOptionsis gone (listtakesPaginationOptions). In Swift,ListNotificationOptionslosesread:. In Rust,ListNotificationsParamsis gone andnotifications().list()takes no params. - Newly required members:
CreateCard.board_id,TriageCard.column_id, and the seven update fields above. Go keeps the types but dropsomitempty, so a zero value is now sent. Ruby gets new required keywords. TS, Kotlin and Swift parameters become non-optional, and Rust goes fromOption<T>toT. A caller that omitted one was getting a 400. - Retries:
PostponeCard,TriageCard,UnTriageCard,UnpinCard,MoveColumnLeft,MoveColumnRightandResetJoinCodeare no longer retried on 429/500/503, because a resend duplicates their effect.
fizzy-cli (the first-party Go consumer, pinned to v0.2.4) builds, vets and passes go test ./... against this branch (replace to this worktree, not committed). fizzy-mcp-server vendors the JSON model and doesn't import the Go module. fizzy-android doesn't use this SDK.
Tests
- Conformance (
conformance/tests/idempotency.json): theMoveColumnLeft"retries on 503" case is reversed to "does NOT retry". Each of the other six operations whose retry policy changed gets its own no-retry case:MoveColumnRight,PostponeCard,TriageCard,UnTriageCard,UnpinCardandResetJoinCode. TheCreateCardfixtures now carryboard_idand assert that it's sent. Removing the TypeScript mapping makes both fail. All 134 cases pass in all six runners. - Runners: TS gains
PostponeCardandUnpinCard. The Swift, Kotlin, Ruby and TS runners are updated for the removed and required members. Swift gainsint32Param. - Smithy's
HttpMethodSemantics.MissingIdempotentTraitwarning is suppressed on the three DELETEs that aren't idempotent, and the operation doc of each says why.
CI failures that come from main
These fail on this PR without touching anything it changes:
- Rubric Check:
rubric-audit.jsonis dated 2026-09-09, which is more than 30 days ago. - Rust Deny / cargo deny: RUSTSEC-2026-0346 through 0349 in rustls, in
rust/Cargo.lock, which this PR doesn't change. - Go Vulnerability Check / Trivy (Go): Go 1.26.6 stdlib advisories (
net/http,crypto/tls,net/textproto).
Refreshing the audit, bumping dependencies and bumping the toolchain each need their own PR. I left them out of a model correction.
Not changed
spec/api-provenance.jsonstill says08395fa. This PR audits the existing operations against27d0d2e, but it isn't a full sync. Fizzy added routes and docs between the two commits (resource :transfer_token, the sole-owner 422 on user deactivation indocs/api/sections/users.md). Moving the revision forward would hide them frommake sync-status.- No release.
Context: basecamp/fizzy-mcp-server#9 lands as it is, since the flat catalog is opt-in (--catalog flat). The resync after this releases is what retires the corrections.