From 73542411164996c135ff6cae06d2a1d4bb3b3345 Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Fri, 18 Sep 2026 12:04:32 -0700 Subject: [PATCH 1/6] Split the Automation catch-all into six real domains MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Automation carried 37 operations across templates, the template library, webhooks, dock tools, questionnaires, lineup markers and search — the widest and least coherent tag in the spec, and about to be frozen. basecamp/mcp's catalog.Load builds one MCP domain tool per tag, so at cutover this becomes one tool spanning all seven surfaces, and retagging afterwards moves a tool people are already using. The six new tags are the service grouping the SDKs already generate. Every tag-keyed generator resolves an operation's service through SERVICE_SPLITS under its tag, and each of these already had its own entry there, so the tag was the only thing holding them together. Tags move; services do not. All seven SDKs regenerate byte-identical. Checkins is a merge, not a new tag. #922 gave six question operations that tag and noted that folding in the questionnaire/question/answer CRUD was a follow-up left to the maintainers; this is it. Both halves already generated into one CheckinsService, so only the tag disagreed. Dock is named for the project tool strip rather than for the Tools service these route to: a consumer building one tool per tag would otherwise get a tool called "tools", and the operations are about the strip, not about the message board or to-do set behind each entry. ListLineupMarkers keeps its Automation service. It was the one operation the tag reached without a SERVICE_SPLITS entry, so it fell through to the tag-derived service while its three siblings were split out, and it has always generated into AutomationService. Retagging it silently would move a public method between services, so each generator now carries an explicit 'Lineup' => { 'Automation' => [ListLineupMarkers] } and Rust an explicit ListLineupMarkers = "Automation" row. Merging it into LineupService is a breaking change and a separate decision. Restating entries are deleted, load-bearing ones are not. The Templates, Webhooks, Search, Lineup and Checkins entries now name their own tag and go dead, as #928's six did; the Tools entry names a service that differs from its new Dock tag and moves with it, as #929's Events entry did. The same distinction drives Rust: 29 names.toml rows restate their tag and are removed, the seven Tools rows do not and stay. The Automation tag no longer exists. openapi.json has no top-level tag registry, so nothing declares it and nothing is left empty. --- go/pkg/basecamp/catalog/catalog.json | 74 ++++----- go/pkg/basecamp/url-routes.json | 42 +++--- go/pkg/basecamp/url_test.go | 73 +++++++++ .../com/basecamp/sdk/generator/Config.kt | 18 +-- openapi.json | 74 ++++----- python/scripts/generate_services.py | 18 +-- ruby/scripts/generate-services.rb | 20 +-- rust/generator/names.toml | 31 +--- spec/overlays/tags.smithy | 141 ++++++++++++------ .../BasecampGenerator/ServiceGrouper.swift | 18 +-- typescript/scripts/generate-services.ts | 18 +-- .../src/generated/openapi-stripped.json | 74 ++++----- 12 files changed, 321 insertions(+), 280 deletions(-) diff --git a/go/pkg/basecamp/catalog/catalog.json b/go/pkg/basecamp/catalog/catalog.json index bd8b41012..e29dce8a8 100644 --- a/go/pkg/basecamp/catalog/catalog.json +++ b/go/pkg/basecamp/catalog/catalog.json @@ -126,7 +126,7 @@ }, { "operationId": "CreateAnswer", - "tag": "Automation", + "tag": "Checkins", "method": "POST", "path": "/{accountId}/questions/{questionId}/answers.json", "doc": "Create a new answer for a question", @@ -1143,7 +1143,7 @@ }, { "operationId": "CreateLineupMarker", - "tag": "Automation", + "tag": "Lineup", "method": "POST", "path": "/{accountId}/lineup/markers.json", "doc": "Create a new lineup marker", @@ -1365,7 +1365,7 @@ }, { "operationId": "CreateProjectFromTemplate", - "tag": "Automation", + "tag": "Templates", "method": "POST", "path": "/{accountId}/templates/{templateId}/project_constructions.json", "doc": "Create a project from a template (asynchronous)", @@ -1432,7 +1432,7 @@ }, { "operationId": "CreateQuestion", - "tag": "Automation", + "tag": "Checkins", "method": "POST", "path": "/{accountId}/questionnaires/{questionnaireId}/questions.json", "doc": "Create a new question in a questionnaire", @@ -1713,7 +1713,7 @@ }, { "operationId": "CreateTemplate", - "tag": "Automation", + "tag": "Templates", "method": "POST", "path": "/{accountId}/templates.json", "doc": "Create a new template", @@ -1759,7 +1759,7 @@ }, { "operationId": "CreateTemplateLibraryCopy", - "tag": "Automation", + "tag": "Templates", "method": "POST", "path": "/{accountId}/template_library/copies.json", "doc": "Start copying a to-do list template into a project", @@ -2150,7 +2150,7 @@ }, { "operationId": "CreateTool", - "tag": "Automation", + "tag": "Dock", "method": "POST", "path": "/{accountId}/buckets/{bucketId}/dock/tools.json", "doc": "Create a tool in a project dock", @@ -2403,7 +2403,7 @@ }, { "operationId": "CreateWebhook", - "tag": "Automation", + "tag": "Webhooks", "method": "POST", "path": "/{accountId}/buckets/{bucketId}/webhooks.json", "doc": "Create a new webhook for a project", @@ -2795,7 +2795,7 @@ }, { "operationId": "DeleteLineupMarker", - "tag": "Automation", + "tag": "Lineup", "method": "DELETE", "path": "/{accountId}/lineup/markers/{markerId}", "doc": "Delete a lineup marker", @@ -2884,7 +2884,7 @@ }, { "operationId": "DeleteTemplate", - "tag": "Automation", + "tag": "Templates", "method": "DELETE", "path": "/{accountId}/templates/{templateId}", "doc": "Delete a template (trash it)", @@ -2924,7 +2924,7 @@ }, { "operationId": "DeleteTool", - "tag": "Automation", + "tag": "Dock", "method": "DELETE", "path": "/{accountId}/dock/tools/{toolId}", "doc": "Delete a tool (trash it)", @@ -2964,7 +2964,7 @@ }, { "operationId": "DeleteWebhook", - "tag": "Automation", + "tag": "Webhooks", "method": "DELETE", "path": "/{accountId}/webhooks/{webhookId}", "doc": "Delete a webhook", @@ -3302,7 +3302,7 @@ }, { "operationId": "DisableTool", - "tag": "Automation", + "tag": "Dock", "method": "DELETE", "path": "/{accountId}/recordings/{toolId}/position.json", "doc": "Disable a tool (hide it from the project dock)", @@ -3497,7 +3497,7 @@ }, { "operationId": "EnableTool", - "tag": "Automation", + "tag": "Dock", "method": "POST", "path": "/{accountId}/recordings/{toolId}/position.json", "doc": "Enable a tool (show it on the project dock)", @@ -3568,7 +3568,7 @@ }, { "operationId": "GetAnswer", - "tag": "Automation", + "tag": "Checkins", "method": "GET", "path": "/{accountId}/question_answers/{answerId}", "doc": "Get a single answer by id", @@ -6319,7 +6319,7 @@ }, { "operationId": "GetProjectConstruction", - "tag": "Automation", + "tag": "Templates", "method": "GET", "path": "/{accountId}/templates/{templateId}/project_constructions/{constructionId}", "doc": "Get the status of a project construction", @@ -6498,7 +6498,7 @@ }, { "operationId": "GetQuestion", - "tag": "Automation", + "tag": "Checkins", "method": "GET", "path": "/{accountId}/questions/{questionId}", "doc": "Get a single question by id", @@ -6583,7 +6583,7 @@ }, { "operationId": "GetQuestionnaire", - "tag": "Automation", + "tag": "Checkins", "method": "GET", "path": "/{accountId}/questionnaires/{questionnaireId}", "doc": "Get a questionnaire (automatic check-ins container) by id", @@ -6827,7 +6827,7 @@ }, { "operationId": "GetSearchMetadata", - "tag": "Automation", + "tag": "Search", "method": "GET", "path": "/{accountId}/searches/metadata.json", "doc": "Get search metadata (available filter options)", @@ -6898,7 +6898,7 @@ }, { "operationId": "GetTemplate", - "tag": "Automation", + "tag": "Templates", "method": "GET", "path": "/{accountId}/templates/{templateId}", "doc": "Get a single template by id", @@ -6938,7 +6938,7 @@ }, { "operationId": "GetTemplateLibrary", - "tag": "Automation", + "tag": "Templates", "method": "GET", "path": "/{accountId}/template_library.json", "doc": "Get the account's to-do list template library", @@ -6969,7 +6969,7 @@ }, { "operationId": "GetTemplateLibraryCopy", - "tag": "Automation", + "tag": "Templates", "method": "GET", "path": "/{accountId}/template_library/copies/{copyId}", "doc": "Get the current status of a to-do list template copy", @@ -7222,7 +7222,7 @@ }, { "operationId": "GetTool", - "tag": "Automation", + "tag": "Dock", "method": "GET", "path": "/{accountId}/dock/tools/{toolId}", "doc": "Get a dock tool by id", @@ -7393,7 +7393,7 @@ }, { "operationId": "GetWebhook", - "tag": "Automation", + "tag": "Webhooks", "method": "GET", "path": "/{accountId}/webhooks/{webhookId}", "doc": "Get a single webhook by id", @@ -7433,7 +7433,7 @@ }, { "operationId": "ListAnswers", - "tag": "Automation", + "tag": "Checkins", "method": "GET", "path": "/{accountId}/questions/{questionId}/answers.json", "doc": "List all answers for a question\n\n**Pagination**: Uses Link header (RFC5988). Follow the `next` rel URL\nto fetch additional pages. X-Total-Count header provides total count.", @@ -8511,7 +8511,7 @@ }, { "operationId": "ListLineupMarkers", - "tag": "Automation", + "tag": "Lineup", "method": "GET", "path": "/{accountId}/lineup/markers.json", "doc": "List all lineup markers for the account", @@ -8980,7 +8980,7 @@ }, { "operationId": "ListQuestions", - "tag": "Automation", + "tag": "Checkins", "method": "GET", "path": "/{accountId}/questionnaires/{questionnaireId}/questions.json", "doc": "List all questions in a questionnaire\n\n**Pagination**: Uses Link header (RFC5988). Follow the `next` rel URL\nto fetch additional pages. X-Total-Count header provides total count.", @@ -9271,7 +9271,7 @@ }, { "operationId": "ListTemplates", - "tag": "Automation", + "tag": "Templates", "method": "GET", "path": "/{accountId}/templates.json", "doc": "List all templates visible to the current user\n\n**Pagination**: Uses Link header (RFC5988). Follow the `next` rel URL\nto fetch additional pages. X-Total-Count header provides total count.", @@ -9664,7 +9664,7 @@ }, { "operationId": "ListWebhooks", - "tag": "Automation", + "tag": "Webhooks", "method": "GET", "path": "/{accountId}/buckets/{bucketId}/webhooks.json", "doc": "List all webhooks for a project\n\n**Pagination**: Uses Link header (RFC5988). Follow the `next` rel URL\nto fetch additional pages. X-Total-Count header provides total count.", @@ -10765,7 +10765,7 @@ }, { "operationId": "RepositionTool", - "tag": "Automation", + "tag": "Dock", "method": "PUT", "path": "/{accountId}/recordings/{toolId}/position.json", "doc": "Reposition a tool on the project dock", @@ -10858,7 +10858,7 @@ }, { "operationId": "Search", - "tag": "Automation", + "tag": "Search", "method": "GET", "path": "/{accountId}/search.json", "doc": "Search for content across the account", @@ -11800,7 +11800,7 @@ }, { "operationId": "UpdateAnswer", - "tag": "Automation", + "tag": "Checkins", "method": "PUT", "path": "/{accountId}/question_answers/{answerId}", "doc": "Update an existing answer", @@ -12599,7 +12599,7 @@ }, { "operationId": "UpdateLineupMarker", - "tag": "Automation", + "tag": "Lineup", "method": "PUT", "path": "/{accountId}/lineup/markers/{markerId}", "doc": "Update an existing lineup marker", @@ -13199,7 +13199,7 @@ }, { "operationId": "UpdateQuestion", - "tag": "Automation", + "tag": "Checkins", "method": "PUT", "path": "/{accountId}/questions/{questionId}", "doc": "Update an existing question", @@ -13455,7 +13455,7 @@ }, { "operationId": "UpdateTemplate", - "tag": "Automation", + "tag": "Templates", "method": "PUT", "path": "/{accountId}/templates/{templateId}", "doc": "Update an existing template", @@ -13625,7 +13625,7 @@ }, { "operationId": "UpdateTool", - "tag": "Automation", + "tag": "Dock", "method": "PUT", "path": "/{accountId}/dock/tools/{toolId}", "doc": "Update (rename) an existing tool", @@ -13776,7 +13776,7 @@ }, { "operationId": "UpdateWebhook", - "tag": "Automation", + "tag": "Webhooks", "method": "PUT", "path": "/{accountId}/webhooks/{webhookId}", "doc": "Update an existing webhook", diff --git a/go/pkg/basecamp/url-routes.json b/go/pkg/basecamp/url-routes.json index f86ea1eaa..da2c5ad3e 100644 --- a/go/pkg/basecamp/url-routes.json +++ b/go/pkg/basecamp/url-routes.json @@ -332,7 +332,7 @@ }, { "pattern": "/{accountId}/buckets/{bucketId}/dock/tools", - "resource": "Automation", + "resource": "Dock", "operations": { "POST": "CreateTool" }, @@ -412,7 +412,7 @@ }, { "pattern": "/{accountId}/buckets/{bucketId}/webhooks", - "resource": "Automation", + "resource": "Webhooks", "operations": { "GET": "ListWebhooks", "POST": "CreateWebhook" @@ -933,7 +933,7 @@ }, { "pattern": "/{accountId}/dock/tools/{toolId}", - "resource": "Automation", + "resource": "Dock", "operations": { "DELETE": "DeleteTool", "GET": "GetTool", @@ -1161,7 +1161,7 @@ }, { "pattern": "/{accountId}/lineup/markers", - "resource": "Automation", + "resource": "Lineup", "operations": { "GET": "ListLineupMarkers", "POST": "CreateLineupMarker" @@ -1175,7 +1175,7 @@ }, { "pattern": "/{accountId}/lineup/markers/{markerId}", - "resource": "Automation", + "resource": "Lineup", "operations": { "DELETE": "DeleteLineupMarker", "PUT": "UpdateLineupMarker" @@ -1758,7 +1758,7 @@ }, { "pattern": "/{accountId}/question_answers/{answerId}", - "resource": "Automation", + "resource": "Checkins", "operations": { "GET": "GetAnswer", "PUT": "UpdateAnswer" @@ -1776,7 +1776,7 @@ }, { "pattern": "/{accountId}/questionnaires/{questionnaireId}", - "resource": "Automation", + "resource": "Checkins", "operations": { "GET": "GetQuestionnaire" }, @@ -1793,7 +1793,7 @@ }, { "pattern": "/{accountId}/questionnaires/{questionnaireId}/questions", - "resource": "Automation", + "resource": "Checkins", "operations": { "GET": "ListQuestions", "POST": "CreateQuestion" @@ -1811,7 +1811,7 @@ }, { "pattern": "/{accountId}/questions/{questionId}", - "resource": "Automation", + "resource": "Checkins", "operations": { "GET": "GetQuestion", "PUT": "UpdateQuestion" @@ -1829,7 +1829,7 @@ }, { "pattern": "/{accountId}/questions/{questionId}/answers", - "resource": "Automation", + "resource": "Checkins", "operations": { "GET": "ListAnswers", "POST": "CreateAnswer" @@ -2190,7 +2190,7 @@ }, { "pattern": "/{accountId}/recordings/{toolId}/position", - "resource": "Automation", + "resource": "Dock", "operations": { "DELETE": "DisableTool", "POST": "EnableTool", @@ -2396,7 +2396,7 @@ }, { "pattern": "/{accountId}/search", - "resource": "Automation", + "resource": "Search", "operations": { "GET": "Search" }, @@ -2409,7 +2409,7 @@ }, { "pattern": "/{accountId}/searches/metadata", - "resource": "Automation", + "resource": "Search", "operations": { "GET": "GetSearchMetadata" }, @@ -2455,7 +2455,7 @@ }, { "pattern": "/{accountId}/template_library", - "resource": "Automation", + "resource": "Templates", "operations": { "GET": "GetTemplateLibrary" }, @@ -2468,7 +2468,7 @@ }, { "pattern": "/{accountId}/template_library/copies", - "resource": "Automation", + "resource": "Templates", "operations": { "POST": "CreateTemplateLibraryCopy" }, @@ -2481,7 +2481,7 @@ }, { "pattern": "/{accountId}/template_library/copies/{copyId}", - "resource": "Automation", + "resource": "Templates", "operations": { "GET": "GetTemplateLibraryCopy" }, @@ -2498,7 +2498,7 @@ }, { "pattern": "/{accountId}/templates", - "resource": "Automation", + "resource": "Templates", "operations": { "GET": "ListTemplates", "POST": "CreateTemplate" @@ -2512,7 +2512,7 @@ }, { "pattern": "/{accountId}/templates/{templateId}", - "resource": "Automation", + "resource": "Templates", "operations": { "DELETE": "DeleteTemplate", "GET": "GetTemplate", @@ -2531,7 +2531,7 @@ }, { "pattern": "/{accountId}/templates/{templateId}/project_constructions", - "resource": "Automation", + "resource": "Templates", "operations": { "POST": "CreateProjectFromTemplate" }, @@ -2548,7 +2548,7 @@ }, { "pattern": "/{accountId}/templates/{templateId}/project_constructions/{constructionId}", - "resource": "Automation", + "resource": "Templates", "operations": { "GET": "GetProjectConstruction" }, @@ -2971,7 +2971,7 @@ }, { "pattern": "/{accountId}/webhooks/{webhookId}", - "resource": "Automation", + "resource": "Webhooks", "operations": { "DELETE": "DeleteWebhook", "GET": "GetWebhook", diff --git a/go/pkg/basecamp/url_test.go b/go/pkg/basecamp/url_test.go index 2e448e59c..278deb108 100644 --- a/go/pkg/basecamp/url_test.go +++ b/go/pkg/basecamp/url_test.go @@ -341,6 +341,79 @@ func TestRouterMatch(t *testing.T) { wantRes: "789", wantResource: "Recordings", }, + // The Automation tag was split into six domains, and Resource is the + // tag. One route per new label is pinned here for the same reason the + // recording routes above are: Match.Resource is public, and the route + // table it comes from is regenerated, so nothing else would notice a + // label going back. + { + name: "dock tool URL", + input: "https://3.basecamp.com/123/dock/tools/789", + wantSource: MatchedAPI, + wantOp: "GetTool", + wantAccount: "123", + wantRes: "789", + wantResource: "Dock", + }, + { + name: "tool position URL", + input: "https://3.basecamp.com/123/recordings/789/position", + wantSource: MatchedAPI, + wantAccount: "123", + wantRes: "789", + wantResource: "Dock", + }, + { + name: "webhook URL", + input: "https://3.basecamp.com/123/webhooks/789", + wantSource: MatchedAPI, + wantOp: "GetWebhook", + wantAccount: "123", + wantRes: "789", + wantResource: "Webhooks", + }, + { + name: "template URL", + input: "https://3.basecamp.com/123/templates/789", + wantSource: MatchedAPI, + wantOp: "GetTemplate", + wantAccount: "123", + wantRes: "789", + wantResource: "Templates", + }, + { + name: "template library URL", + input: "https://3.basecamp.com/123/template_library", + wantSource: MatchedAPI, + wantOp: "GetTemplateLibrary", + wantAccount: "123", + wantResource: "Templates", + }, + { + name: "lineup markers URL", + input: "https://3.basecamp.com/123/lineup/markers", + wantSource: MatchedAPI, + wantOp: "ListLineupMarkers", + wantAccount: "123", + wantResource: "Lineup", + }, + { + name: "search URL", + input: "https://3.basecamp.com/123/search", + wantSource: MatchedAPI, + wantOp: "Search", + wantAccount: "123", + wantResource: "Search", + }, + { + name: "questionnaire URL", + input: "https://3.basecamp.com/123/questionnaires/789", + wantSource: MatchedAPI, + wantOp: "GetQuestionnaire", + wantAccount: "123", + wantRes: "789", + wantResource: "Checkins", + }, } for _, tt := range tests { diff --git a/kotlin/generator/src/main/kotlin/com/basecamp/sdk/generator/Config.kt b/kotlin/generator/src/main/kotlin/com/basecamp/sdk/generator/Config.kt index 292035dbd..0c7728b2f 100644 --- a/kotlin/generator/src/main/kotlin/com/basecamp/sdk/generator/Config.kt +++ b/kotlin/generator/src/main/kotlin/com/basecamp/sdk/generator/Config.kt @@ -14,7 +14,6 @@ val TAG_TO_SERVICE = mapOf( "Schedule" to "Schedules", "People" to "People", "Projects" to "Projects", - "Automation" to "Automation", "ClientFeatures" to "ClientFeatures", "Boosts" to "Boosts", "Untagged" to "Miscellaneous", @@ -55,20 +54,11 @@ val SERVICE_SPLITS: Map>> = mapOf( "CloudFiles" to listOf("GetCloudFile", "CreateCloudFile", "UpdateCloudFile"), "GoogleDocuments" to listOf("GetGoogleDocument", "CreateGoogleDocument", "UpdateGoogleDocument"), ), - "Automation" to mapOf( + "Dock" to mapOf( "Tools" to listOf("GetTool", "UpdateTool", "DeleteTool", "CreateTool", "EnableTool", "DisableTool", "RepositionTool"), - "Webhooks" to listOf("ListWebhooks", "CreateWebhook", "GetWebhook", "UpdateWebhook", "DeleteWebhook"), - "Lineup" to listOf("CreateLineupMarker", "UpdateLineupMarker", "DeleteLineupMarker"), - "Search" to listOf("Search", "GetSearchMetadata"), - "Templates" to listOf( - "ListTemplates", "CreateTemplate", "GetTemplate", "UpdateTemplate", - "DeleteTemplate", "CreateProjectFromTemplate", "GetProjectConstruction", - "GetTemplateLibrary", "CreateTemplateLibraryCopy", "GetTemplateLibraryCopy", - ), - "Checkins" to listOf( - "GetQuestionnaire", "ListQuestions", "CreateQuestion", "GetQuestion", - "UpdateQuestion", "ListAnswers", "CreateAnswer", "GetAnswer", "UpdateAnswer", - ), + ), + "Lineup" to mapOf( + "Automation" to listOf("ListLineupMarkers"), ), "Recordings" to mapOf( "Events" to listOf("ListEvents"), diff --git a/openapi.json b/openapi.json index 29c1d3cdb..2194577bf 100644 --- a/openapi.json +++ b/openapi.json @@ -3059,7 +3059,7 @@ } }, "tags": [ - "Automation" + "Dock" ], "x-basecamp-retry": { "maxAttempts": 2, @@ -3514,7 +3514,7 @@ } }, "tags": [ - "Automation" + "Webhooks" ], "x-basecamp-pagination": { "style": "link", @@ -3639,7 +3639,7 @@ } }, "tags": [ - "Automation" + "Webhooks" ], "x-basecamp-retry": { "maxAttempts": 2, @@ -8308,7 +8308,7 @@ } }, "tags": [ - "Automation" + "Dock" ], "x-basecamp-idempotent": { "natural": true @@ -8401,7 +8401,7 @@ } }, "tags": [ - "Automation" + "Dock" ], "x-basecamp-retry": { "maxAttempts": 3, @@ -8511,7 +8511,7 @@ } }, "tags": [ - "Automation" + "Dock" ], "x-basecamp-idempotent": { "natural": true @@ -10494,7 +10494,7 @@ } }, "tags": [ - "Automation" + "Lineup" ], "x-basecamp-retry": { "maxAttempts": 3, @@ -10588,7 +10588,7 @@ } }, "tags": [ - "Automation" + "Lineup" ], "x-basecamp-retry": { "maxAttempts": 2, @@ -10673,7 +10673,7 @@ } }, "tags": [ - "Automation" + "Lineup" ], "x-basecamp-idempotent": { "natural": true @@ -10778,7 +10778,7 @@ } }, "tags": [ - "Automation" + "Lineup" ], "x-basecamp-idempotent": { "natural": true @@ -16125,7 +16125,7 @@ } }, "tags": [ - "Automation" + "Checkins" ], "x-basecamp-retry": { "maxAttempts": 3, @@ -16228,7 +16228,7 @@ } }, "tags": [ - "Automation" + "Checkins" ], "x-basecamp-idempotent": { "natural": true @@ -16323,7 +16323,7 @@ } }, "tags": [ - "Automation" + "Checkins" ], "x-basecamp-retry": { "maxAttempts": 3, @@ -16425,7 +16425,7 @@ } }, "tags": [ - "Automation" + "Checkins" ], "x-basecamp-pagination": { "style": "link", @@ -16540,7 +16540,7 @@ } }, "tags": [ - "Automation" + "Checkins" ], "x-basecamp-retry": { "maxAttempts": 2, @@ -16632,7 +16632,7 @@ } }, "tags": [ - "Automation" + "Checkins" ], "x-basecamp-retry": { "maxAttempts": 3, @@ -16741,7 +16741,7 @@ } }, "tags": [ - "Automation" + "Checkins" ], "x-basecamp-idempotent": { "natural": true @@ -16846,7 +16846,7 @@ } }, "tags": [ - "Automation" + "Checkins" ], "x-basecamp-pagination": { "style": "link", @@ -16961,7 +16961,7 @@ } }, "tags": [ - "Automation" + "Checkins" ], "x-basecamp-retry": { "maxAttempts": 2, @@ -20394,7 +20394,7 @@ } }, "tags": [ - "Automation" + "Dock" ], "x-basecamp-idempotent": { "natural": true @@ -20490,7 +20490,7 @@ } }, "tags": [ - "Automation" + "Dock" ], "x-basecamp-retry": { "maxAttempts": 2, @@ -20593,7 +20593,7 @@ } }, "tags": [ - "Automation" + "Dock" ], "x-basecamp-idempotent": { "natural": true @@ -22378,7 +22378,7 @@ } }, "tags": [ - "Automation" + "Search" ], "x-basecamp-pagination": { "style": "link", @@ -22466,7 +22466,7 @@ } }, "tags": [ - "Automation" + "Search" ], "x-basecamp-retry": { "maxAttempts": 3, @@ -23043,7 +23043,7 @@ } }, "tags": [ - "Automation" + "Templates" ], "x-basecamp-retry": { "maxAttempts": 3, @@ -23156,7 +23156,7 @@ } }, "tags": [ - "Automation" + "Templates" ], "x-basecamp-retry": { "maxAttempts": 2, @@ -23258,7 +23258,7 @@ } }, "tags": [ - "Automation" + "Templates" ], "x-basecamp-retry": { "maxAttempts": 3, @@ -23360,7 +23360,7 @@ } }, "tags": [ - "Automation" + "Templates" ], "x-basecamp-pagination": { "style": "link", @@ -23466,7 +23466,7 @@ } }, "tags": [ - "Automation" + "Templates" ], "x-basecamp-retry": { "maxAttempts": 2, @@ -23551,7 +23551,7 @@ } }, "tags": [ - "Automation" + "Templates" ], "x-basecamp-idempotent": { "natural": true @@ -23644,7 +23644,7 @@ } }, "tags": [ - "Automation" + "Templates" ], "x-basecamp-retry": { "maxAttempts": 3, @@ -23753,7 +23753,7 @@ } }, "tags": [ - "Automation" + "Templates" ], "x-basecamp-idempotent": { "natural": true @@ -23868,7 +23868,7 @@ } }, "tags": [ - "Automation" + "Templates" ], "x-basecamp-retry": { "maxAttempts": 2, @@ -23969,7 +23969,7 @@ } }, "tags": [ - "Automation" + "Templates" ], "x-basecamp-retry": { "maxAttempts": 3, @@ -28253,7 +28253,7 @@ } }, "tags": [ - "Automation" + "Webhooks" ], "x-basecamp-idempotent": { "natural": true @@ -28346,7 +28346,7 @@ } }, "tags": [ - "Automation" + "Webhooks" ], "x-basecamp-retry": { "maxAttempts": 3, @@ -28465,7 +28465,7 @@ } }, "tags": [ - "Automation" + "Webhooks" ], "x-basecamp-idempotent": { "natural": true diff --git a/python/scripts/generate_services.py b/python/scripts/generate_services.py index 3fd9f6330..c52580397 100644 --- a/python/scripts/generate_services.py +++ b/python/scripts/generate_services.py @@ -42,7 +42,6 @@ "Schedule": "Schedules", "People": "People", "Projects": "Projects", - "Automation": "Automation", "ClientFeatures": "ClientFeatures", "Boosts": "Boosts", "Untagged": "Miscellaneous", @@ -81,20 +80,11 @@ "CloudFiles": ["GetCloudFile", "CreateCloudFile", "UpdateCloudFile"], "GoogleDocuments": ["GetGoogleDocument", "CreateGoogleDocument", "UpdateGoogleDocument"], }, - "Automation": { + "Dock": { "Tools": ["GetTool", "UpdateTool", "DeleteTool", "CreateTool", "EnableTool", "DisableTool", "RepositionTool"], - "Webhooks": ["ListWebhooks", "CreateWebhook", "GetWebhook", "UpdateWebhook", "DeleteWebhook"], - "Lineup": ["CreateLineupMarker", "UpdateLineupMarker", "DeleteLineupMarker"], - "Search": ["Search", "GetSearchMetadata"], - "Templates": [ - "ListTemplates", "CreateTemplate", "GetTemplate", "UpdateTemplate", - "DeleteTemplate", "CreateProjectFromTemplate", "GetProjectConstruction", - "GetTemplateLibrary", "CreateTemplateLibraryCopy", "GetTemplateLibraryCopy", - ], - "Checkins": [ - "GetQuestionnaire", "ListQuestions", "CreateQuestion", "GetQuestion", - "UpdateQuestion", "ListAnswers", "CreateAnswer", "GetAnswer", "UpdateAnswer", - ], + }, + "Lineup": { + "Automation": ["ListLineupMarkers"], }, "Recordings": { "Events": ["ListEvents"], diff --git a/ruby/scripts/generate-services.rb b/ruby/scripts/generate-services.rb index 350e05590..bde30a3c1 100644 --- a/ruby/scripts/generate-services.rb +++ b/ruby/scripts/generate-services.rb @@ -69,7 +69,6 @@ class ServiceGenerator 'Schedule' => 'Schedules', 'People' => 'People', 'Projects' => 'Projects', - 'Automation' => 'Automation', 'ClientFeatures' => 'ClientFeatures', 'Boosts' => 'Boosts', 'Untagged' => 'Miscellaneous' @@ -108,20 +107,11 @@ class ServiceGenerator 'CloudFiles' => %w[GetCloudFile CreateCloudFile UpdateCloudFile], 'GoogleDocuments' => %w[GetGoogleDocument CreateGoogleDocument UpdateGoogleDocument] }, - 'Automation' => { - 'Tools' => %w[GetTool UpdateTool DeleteTool CreateTool EnableTool DisableTool RepositionTool], - 'Webhooks' => %w[ListWebhooks CreateWebhook GetWebhook UpdateWebhook DeleteWebhook], - 'Lineup' => %w[CreateLineupMarker UpdateLineupMarker DeleteLineupMarker], - 'Search' => %w[Search GetSearchMetadata], - 'Templates' => %w[ - ListTemplates CreateTemplate GetTemplate UpdateTemplate - DeleteTemplate CreateProjectFromTemplate GetProjectConstruction - GetTemplateLibrary CreateTemplateLibraryCopy GetTemplateLibraryCopy - ], - 'Checkins' => %w[ - GetQuestionnaire ListQuestions CreateQuestion GetQuestion - UpdateQuestion ListAnswers CreateAnswer GetAnswer UpdateAnswer - ] + 'Dock' => { + 'Tools' => %w[GetTool UpdateTool DeleteTool CreateTool EnableTool DisableTool RepositionTool] + }, + 'Lineup' => { + 'Automation' => %w[ListLineupMarkers] }, 'Recordings' => { 'Events' => %w[ListEvents] diff --git a/rust/generator/names.toml b/rust/generator/names.toml index d745f14ad..c78e0edd4 100644 --- a/rust/generator/names.toml +++ b/rust/generator/names.toml @@ -21,7 +21,6 @@ "Schedule" = "Schedules" "People" = "People" "Projects" = "Projects" -"Automation" = "Automation" "ClientFeatures" = "ClientFeatures" "Boosts" = "Boosts" "Untagged" = "Miscellaneous" @@ -30,6 +29,7 @@ # says. Ordered by service, then operation, so a diff against Config.kt reads in blocks. [operation_services] CreateAttachment = "Attachments" +ListLineupMarkers = "Automation" CreateCampfireLine = "Campfires" CreateCampfireUpload = "Campfires" CreateChatbot = "Campfires" @@ -64,20 +64,11 @@ GetCard = "Cards" ListCards = "Cards" MoveCard = "Cards" UpdateCard = "Cards" -CreateAnswer = "Checkins" -CreateQuestion = "Checkins" -GetAnswer = "Checkins" GetAnswersByPerson = "Checkins" -GetQuestion = "Checkins" GetQuestionReminders = "Checkins" -GetQuestionnaire = "Checkins" -ListAnswers = "Checkins" ListQuestionAnswerers = "Checkins" -ListQuestions = "Checkins" PauseQuestion = "Checkins" ResumeQuestion = "Checkins" -UpdateAnswer = "Checkins" -UpdateQuestion = "Checkins" UpdateQuestionNotificationSettings = "Checkins" GetClientApproval = "ClientApprovals" ListClientApprovals = "ClientApprovals" @@ -103,9 +94,6 @@ GetGoogleDocument = "GoogleDocuments" UpdateGoogleDocument = "GoogleDocuments" GetHillChart = "HillCharts" UpdateHillChartSettings = "HillCharts" -CreateLineupMarker = "Lineup" -DeleteLineupMarker = "Lineup" -UpdateLineupMarker = "Lineup" GetMessageBoard = "MessageBoards" CreateMessageType = "MessageTypes" DeleteMessageType = "MessageTypes" @@ -137,22 +125,10 @@ GetScheduleEntryOccurrence = "Schedules" ListScheduleEntries = "Schedules" ReplaceScheduleEntry = "Schedules" UpdateScheduleSettings = "Schedules" -GetSearchMetadata = "Search" -Search = "Search" GetSubscription = "Subscriptions" Subscribe = "Subscriptions" Unsubscribe = "Subscriptions" UpdateSubscription = "Subscriptions" -CreateProjectFromTemplate = "Templates" -CreateTemplate = "Templates" -CreateTemplateLibraryCopy = "Templates" -DeleteTemplate = "Templates" -GetProjectConstruction = "Templates" -GetTemplate = "Templates" -GetTemplateLibrary = "Templates" -GetTemplateLibraryCopy = "Templates" -ListTemplates = "Templates" -UpdateTemplate = "Templates" GetProjectTimeline = "Timeline" CreateTimesheetEntry = "Timesheets" DestroyTimesheetEntry = "Timesheets" @@ -195,11 +171,6 @@ CreateVault = "Vaults" GetVault = "Vaults" ListVaults = "Vaults" UpdateVault = "Vaults" -CreateWebhook = "Webhooks" -DeleteWebhook = "Webhooks" -GetWebhook = "Webhooks" -ListWebhooks = "Webhooks" -UpdateWebhook = "Webhooks" CreateWormhole = "Wormholes" DeleteWormhole = "Wormholes" UpdateWormhole = "Wormholes" diff --git a/spec/overlays/tags.smithy b/spec/overlays/tags.smithy index 886bdf571..2b7579a3b 100644 --- a/spec/overlays/tags.smithy +++ b/spec/overlays/tags.smithy @@ -165,35 +165,81 @@ apply ListClientReplies @tags(["ClientFeatures"]) apply GetClientReply @tags(["ClientFeatures"]) apply SetClientVisibility @tags(["ClientFeatures"]) -// Automation (Webhooks, Search, Templates, Tools, Lineup) -apply ListWebhooks @tags(["Automation"]) -apply GetWebhook @tags(["Automation"]) -apply CreateWebhook @tags(["Automation"]) -apply UpdateWebhook @tags(["Automation"]) -apply DeleteWebhook @tags(["Automation"]) -apply Search @tags(["Automation"]) -apply GetSearchMetadata @tags(["Automation"]) -apply ListTemplates @tags(["Automation"]) -apply GetTemplate @tags(["Automation"]) -apply CreateTemplate @tags(["Automation"]) -apply UpdateTemplate @tags(["Automation"]) -apply DeleteTemplate @tags(["Automation"]) -apply CreateProjectFromTemplate @tags(["Automation"]) -apply GetProjectConstruction @tags(["Automation"]) -apply GetTemplateLibrary @tags(["Automation"]) -apply CreateTemplateLibraryCopy @tags(["Automation"]) -apply GetTemplateLibraryCopy @tags(["Automation"]) -apply GetTool @tags(["Automation"]) -apply CreateTool @tags(["Automation"]) -apply UpdateTool @tags(["Automation"]) -apply DeleteTool @tags(["Automation"]) -apply EnableTool @tags(["Automation"]) -apply DisableTool @tags(["Automation"]) -apply RepositionTool @tags(["Automation"]) -apply ListLineupMarkers @tags(["Automation"]) -apply CreateLineupMarker @tags(["Automation"]) -apply UpdateLineupMarker @tags(["Automation"]) -apply DeleteLineupMarker @tags(["Automation"]) +// Webhooks +// +// A per-project subscription: register a callback URL, say which event types it +// wants, read back what is registered. The one surface here that automation is +// actually built on, which is why it is named for the thing rather than for the +// category the thing belongs to. +apply ListWebhooks @tags(["Webhooks"]) +apply GetWebhook @tags(["Webhooks"]) +apply CreateWebhook @tags(["Webhooks"]) +apply UpdateWebhook @tags(["Webhooks"]) +apply DeleteWebhook @tags(["Webhooks"]) + +// Search +// +// Account-wide query over recordings, plus the metadata describing what can be +// filtered on. A read surface, and the only one of the six that touches every +// other domain rather than owning a resource of its own. +apply Search @tags(["Search"]) +apply GetSearchMetadata @tags(["Search"]) + +// Templates (project blueprints and the account template library) +// +// One domain, not two. A template is a stored project shape; a project +// construction is the asynchronous job that builds a project from one. The +// template library is the same act from a different source — the account's +// shared catalogue instead of a template you own — and a library copy is its +// construction. Splitting the library out would put the two halves of "make a +// new project from something stored" under different tools. +apply ListTemplates @tags(["Templates"]) +apply GetTemplate @tags(["Templates"]) +apply CreateTemplate @tags(["Templates"]) +apply UpdateTemplate @tags(["Templates"]) +apply DeleteTemplate @tags(["Templates"]) +apply CreateProjectFromTemplate @tags(["Templates"]) +apply GetProjectConstruction @tags(["Templates"]) +apply GetTemplateLibrary @tags(["Templates"]) +apply CreateTemplateLibraryCopy @tags(["Templates"]) +apply GetTemplateLibraryCopy @tags(["Templates"]) + +// Dock (a project's tool strip) +// +// Which tools a project has, what they are called, what order they sit in, and +// whether they are turned on. Named for the dock rather than for the Tools +// service these route to: a consumer that builds one domain tool per tag would +// otherwise get a tool called "tools", and the dock is what the operations are +// about — the strip, not the message board or the to-do set behind each entry. +// +// EnableTool, DisableTool and RepositionTool sit under /recordings/{toolId}/, +// and the path label is toolId, not recordingId. #929 left them out of the +// Recordings retag for that reason; they are dock operations that share a +// prefix. +apply GetTool @tags(["Dock"]) +apply CreateTool @tags(["Dock"]) +apply UpdateTool @tags(["Dock"]) +apply DeleteTool @tags(["Dock"]) +apply EnableTool @tags(["Dock"]) +apply DisableTool @tags(["Dock"]) +apply RepositionTool @tags(["Dock"]) + +// Lineup (account-wide schedule markers) +// +// ListLineupMarkers is the one operation here whose SERVICE is not Lineup. It +// has always generated into AutomationService — it was the only operation the +// Automation tag reached without a SERVICE_SPLITS entry, so it fell through to +// the tag-derived service while its three siblings were split out. Retagging it +// Lineup without saying so would move a public method between services, so each +// tag-keyed generator now carries 'Lineup' => { 'Automation' => [...] } and the +// Rust table an explicit ListLineupMarkers = "Automation" row. The tag is gone; +// the service it named survives, holding exactly this one method, as it did +// before. Merging it into LineupService is a breaking change and a separate +// decision. +apply ListLineupMarkers @tags(["Lineup"]) +apply CreateLineupMarker @tags(["Lineup"]) +apply UpdateLineupMarker @tags(["Lineup"]) +apply DeleteLineupMarker @tags(["Lineup"]) // Boosts apply ListRecordingBoosts @tags(["Boosts"]) @@ -203,22 +249,23 @@ apply CreateRecordingBoost @tags(["Boosts"]) apply CreateEventBoost @tags(["Boosts"]) apply DeleteBoost @tags(["Boosts"]) -// Checkins (under Automation) -apply GetQuestionnaire @tags(["Automation"]) -apply ListQuestions @tags(["Automation"]) -apply GetQuestion @tags(["Automation"]) -apply CreateQuestion @tags(["Automation"]) -apply UpdateQuestion @tags(["Automation"]) -apply ListAnswers @tags(["Automation"]) -apply GetAnswer @tags(["Automation"]) -apply CreateAnswer @tags(["Automation"]) -apply UpdateAnswer @tags(["Automation"]) - -// Checkins (question reminders, pausing, answerers, notification settings) -// New domain tag: these question operations had no tag, and no existing -// domain tag fit. The questionnaire/question/answer CRUD ops above stay under -// Automation for now; consolidating the whole check-in family under Checkins -// is a follow-up editorial call left to the maintainers. +// Checkins (questionnaires, questions, answers) +// +// #922 created this tag for the six operations that had none and noted that +// consolidating the rest of the check-in family under it was a follow-up +// editorial call. This is that follow-up. The questionnaire/question/answer +// CRUD below already generated into the Checkins SERVICE alongside these six; +// only the tag disagreed, so the two halves of one domain were being offered +// to tag-keyed consumers as two. +apply GetQuestionnaire @tags(["Checkins"]) +apply ListQuestions @tags(["Checkins"]) +apply GetQuestion @tags(["Checkins"]) +apply CreateQuestion @tags(["Checkins"]) +apply UpdateQuestion @tags(["Checkins"]) +apply ListAnswers @tags(["Checkins"]) +apply GetAnswer @tags(["Checkins"]) +apply CreateAnswer @tags(["Checkins"]) +apply UpdateAnswer @tags(["Checkins"]) apply GetQuestionReminders @tags(["Checkins"]) apply ListQuestionAnswerers @tags(["Checkins"]) apply GetAnswersByPerson @tags(["Checkins"]) @@ -353,9 +400,9 @@ apply CreateStreamTicket @tags(["EventFeed"]) // Bookmarks, Boosts, BubbleUps, ClientFeatures, Messages, People and Schedule // operations, and ListEventBoosts (.../events/{eventId}/boosts.json) is Boosts // even though it hangs off this very sub-resource. EnableTool, DisableTool and -// RepositionTool stay Automation: their /{accountId}/recordings/{toolId}/ +// RepositionTool are not Recordings: their /{accountId}/recordings/{toolId}/ // position.json path takes a toolId, not a recordingId — they are dock-tool -// operations that happen to share the prefix. +// operations that happen to share the prefix, and they are tagged Dock. apply ListRecordings @tags(["Recordings"]) apply SpotlightRecording @tags(["Recordings"]) apply UnspotlightRecording @tags(["Recordings"]) diff --git a/swift/Sources/BasecampGenerator/ServiceGrouper.swift b/swift/Sources/BasecampGenerator/ServiceGrouper.swift index eecd2fc87..b375ed0ed 100644 --- a/swift/Sources/BasecampGenerator/ServiceGrouper.swift +++ b/swift/Sources/BasecampGenerator/ServiceGrouper.swift @@ -13,7 +13,6 @@ let tagToService: [String: String] = [ "Schedule": "Schedules", "People": "People", "Projects": "Projects", - "Automation": "Automation", "ClientFeatures": "ClientFeatures", "Boosts": "Boosts", "Untagged": "Miscellaneous", @@ -54,20 +53,11 @@ let serviceSplits: [String: [String: [String]]] = [ "CloudFiles": ["GetCloudFile", "CreateCloudFile", "UpdateCloudFile"], "GoogleDocuments": ["GetGoogleDocument", "CreateGoogleDocument", "UpdateGoogleDocument"], ], - "Automation": [ + "Dock": [ "Tools": ["GetTool", "UpdateTool", "DeleteTool", "CreateTool", "EnableTool", "DisableTool", "RepositionTool"], - "Webhooks": ["ListWebhooks", "CreateWebhook", "GetWebhook", "UpdateWebhook", "DeleteWebhook"], - "Lineup": ["CreateLineupMarker", "UpdateLineupMarker", "DeleteLineupMarker"], - "Search": ["Search", "GetSearchMetadata"], - "Templates": [ - "ListTemplates", "CreateTemplate", "GetTemplate", "UpdateTemplate", - "DeleteTemplate", "CreateProjectFromTemplate", "GetProjectConstruction", - "GetTemplateLibrary", "CreateTemplateLibraryCopy", "GetTemplateLibraryCopy", - ], - "Checkins": [ - "GetQuestionnaire", "ListQuestions", "CreateQuestion", "GetQuestion", - "UpdateQuestion", "ListAnswers", "CreateAnswer", "GetAnswer", "UpdateAnswer", - ], + ], + "Lineup": [ + "Automation": ["ListLineupMarkers"], ], "Recordings": [ "Events": ["ListEvents"], diff --git a/typescript/scripts/generate-services.ts b/typescript/scripts/generate-services.ts index 4875fecc0..74915d635 100644 --- a/typescript/scripts/generate-services.ts +++ b/typescript/scripts/generate-services.ts @@ -174,7 +174,6 @@ const TAG_TO_SERVICE: Record = { Schedule: "Schedules", People: "People", Projects: "Projects", - Automation: "Automation", ClientFeatures: "ClientFeatures", Boosts: "Boosts", Untagged: "Miscellaneous", @@ -215,20 +214,11 @@ const SERVICE_SPLITS: Record> = { CloudFiles: ["GetCloudFile", "CreateCloudFile", "UpdateCloudFile"], GoogleDocuments: ["GetGoogleDocument", "CreateGoogleDocument", "UpdateGoogleDocument"], }, - Automation: { + Dock: { Tools: ["GetTool", "UpdateTool", "DeleteTool", "CreateTool", "EnableTool", "DisableTool", "RepositionTool"], - Webhooks: ["ListWebhooks", "CreateWebhook", "GetWebhook", "UpdateWebhook", "DeleteWebhook"], - Lineup: ["CreateLineupMarker", "UpdateLineupMarker", "DeleteLineupMarker"], - Search: ["Search", "GetSearchMetadata"], - Templates: [ - "ListTemplates", "CreateTemplate", "GetTemplate", "UpdateTemplate", - "DeleteTemplate", "CreateProjectFromTemplate", "GetProjectConstruction", - "GetTemplateLibrary", "CreateTemplateLibraryCopy", "GetTemplateLibraryCopy", - ], - Checkins: [ - "GetQuestionnaire", "ListQuestions", "CreateQuestion", "GetQuestion", - "UpdateQuestion", "ListAnswers", "CreateAnswer", "GetAnswer", "UpdateAnswer", - ], + }, + Lineup: { + Automation: ["ListLineupMarkers"], }, Recordings: { Events: ["ListEvents"], diff --git a/typescript/src/generated/openapi-stripped.json b/typescript/src/generated/openapi-stripped.json index 35d119a8c..0f6fa6698 100644 --- a/typescript/src/generated/openapi-stripped.json +++ b/typescript/src/generated/openapi-stripped.json @@ -2747,7 +2747,7 @@ } }, "tags": [ - "Automation" + "Dock" ], "x-basecamp-retry": { "maxAttempts": 2, @@ -3158,7 +3158,7 @@ } }, "tags": [ - "Automation" + "Webhooks" ], "x-basecamp-pagination": { "style": "link", @@ -3272,7 +3272,7 @@ } }, "tags": [ - "Automation" + "Webhooks" ], "x-basecamp-retry": { "maxAttempts": 2, @@ -7456,7 +7456,7 @@ } }, "tags": [ - "Automation" + "Dock" ], "x-basecamp-idempotent": { "natural": true @@ -7538,7 +7538,7 @@ } }, "tags": [ - "Automation" + "Dock" ], "x-basecamp-retry": { "maxAttempts": 3, @@ -7637,7 +7637,7 @@ } }, "tags": [ - "Automation" + "Dock" ], "x-basecamp-idempotent": { "natural": true @@ -9420,7 +9420,7 @@ } }, "tags": [ - "Automation" + "Lineup" ], "x-basecamp-retry": { "maxAttempts": 3, @@ -9502,7 +9502,7 @@ } }, "tags": [ - "Automation" + "Lineup" ], "x-basecamp-retry": { "maxAttempts": 2, @@ -9576,7 +9576,7 @@ } }, "tags": [ - "Automation" + "Lineup" ], "x-basecamp-idempotent": { "natural": true @@ -9670,7 +9670,7 @@ } }, "tags": [ - "Automation" + "Lineup" ], "x-basecamp-idempotent": { "natural": true @@ -14408,7 +14408,7 @@ } }, "tags": [ - "Automation" + "Checkins" ], "x-basecamp-retry": { "maxAttempts": 3, @@ -14500,7 +14500,7 @@ } }, "tags": [ - "Automation" + "Checkins" ], "x-basecamp-idempotent": { "natural": true @@ -14584,7 +14584,7 @@ } }, "tags": [ - "Automation" + "Checkins" ], "x-basecamp-retry": { "maxAttempts": 3, @@ -14675,7 +14675,7 @@ } }, "tags": [ - "Automation" + "Checkins" ], "x-basecamp-pagination": { "style": "link", @@ -14779,7 +14779,7 @@ } }, "tags": [ - "Automation" + "Checkins" ], "x-basecamp-retry": { "maxAttempts": 2, @@ -14860,7 +14860,7 @@ } }, "tags": [ - "Automation" + "Checkins" ], "x-basecamp-retry": { "maxAttempts": 3, @@ -14958,7 +14958,7 @@ } }, "tags": [ - "Automation" + "Checkins" ], "x-basecamp-idempotent": { "natural": true @@ -15052,7 +15052,7 @@ } }, "tags": [ - "Automation" + "Checkins" ], "x-basecamp-pagination": { "style": "link", @@ -15156,7 +15156,7 @@ } }, "tags": [ - "Automation" + "Checkins" ], "x-basecamp-retry": { "maxAttempts": 2, @@ -18218,7 +18218,7 @@ } }, "tags": [ - "Automation" + "Dock" ], "x-basecamp-idempotent": { "natural": true @@ -18303,7 +18303,7 @@ } }, "tags": [ - "Automation" + "Dock" ], "x-basecamp-retry": { "maxAttempts": 2, @@ -18395,7 +18395,7 @@ } }, "tags": [ - "Automation" + "Dock" ], "x-basecamp-idempotent": { "natural": true @@ -20002,7 +20002,7 @@ } }, "tags": [ - "Automation" + "Search" ], "x-basecamp-pagination": { "style": "link", @@ -20078,7 +20078,7 @@ } }, "tags": [ - "Automation" + "Search" ], "x-basecamp-retry": { "maxAttempts": 3, @@ -20586,7 +20586,7 @@ } }, "tags": [ - "Automation" + "Templates" ], "x-basecamp-retry": { "maxAttempts": 3, @@ -20687,7 +20687,7 @@ } }, "tags": [ - "Automation" + "Templates" ], "x-basecamp-retry": { "maxAttempts": 2, @@ -20778,7 +20778,7 @@ } }, "tags": [ - "Automation" + "Templates" ], "x-basecamp-retry": { "maxAttempts": 3, @@ -20869,7 +20869,7 @@ } }, "tags": [ - "Automation" + "Templates" ], "x-basecamp-pagination": { "style": "link", @@ -20963,7 +20963,7 @@ } }, "tags": [ - "Automation" + "Templates" ], "x-basecamp-retry": { "maxAttempts": 2, @@ -21037,7 +21037,7 @@ } }, "tags": [ - "Automation" + "Templates" ], "x-basecamp-idempotent": { "natural": true @@ -21119,7 +21119,7 @@ } }, "tags": [ - "Automation" + "Templates" ], "x-basecamp-retry": { "maxAttempts": 3, @@ -21217,7 +21217,7 @@ } }, "tags": [ - "Automation" + "Templates" ], "x-basecamp-idempotent": { "natural": true @@ -21321,7 +21321,7 @@ } }, "tags": [ - "Automation" + "Templates" ], "x-basecamp-retry": { "maxAttempts": 2, @@ -21411,7 +21411,7 @@ } }, "tags": [ - "Automation" + "Templates" ], "x-basecamp-retry": { "maxAttempts": 3, @@ -25254,7 +25254,7 @@ } }, "tags": [ - "Automation" + "Webhooks" ], "x-basecamp-idempotent": { "natural": true @@ -25336,7 +25336,7 @@ } }, "tags": [ - "Automation" + "Webhooks" ], "x-basecamp-retry": { "maxAttempts": 3, @@ -25444,7 +25444,7 @@ } }, "tags": [ - "Automation" + "Webhooks" ], "x-basecamp-idempotent": { "natural": true From d9fc93c00c9b31342c11d656171aefdfc6eda8d2 Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Fri, 18 Sep 2026 12:31:25 -0700 Subject: [PATCH 2/6] Stop naming Automation as an example of a coarse tag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SPEC section 5.1 opens by naming three coarse tags the generators split. Automation is no longer one of them. Card Tables is — five services out of one tag — so the example keeps its point. --- SPEC.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SPEC.md b/SPEC.md index b00615469..c8ccc19e9 100644 --- a/SPEC.md +++ b/SPEC.md @@ -307,7 +307,7 @@ That roster is the canonical surface, not a per-SDK inventory. Accessor counts v ### Derivation Rule `[static]` -The OpenAPI spec groups operations under coarse tags (e.g., `Automation`, `Todos`, `Files`). The service generators split those tags into the `55` fine-grained services above using a two-table mapping: `TAG_TO_SERVICE` (tag → default service name) and `SERVICE_SPLITS` (tag → {service → [operationIds]}). For example, the `Todos` tag splits into `Todos`, `Todolists`, `Todosets`, `TodolistGroups`, `HillCharts`; the `Files` tag splits into `Attachments`, `Uploads`, `Vaults`, `Documents`, `CloudFiles`, `GoogleDocuments`. Both examples are exhaustive on purpose: an abridged one is how `cloudFiles` and `googleDocuments` stayed invisible to this section for so long — a service that arrives through a split rather than a tag of its own is named nowhere a reader would look. These mappings are defined in each language's generator script. They are six hand-maintained copies of one table (Rust's is the `names.toml` its generator reads), and `make check-service-inventory-parity` compares what those copies **emitted** — the TypeScript, Ruby, Kotlin, Swift and Rust generated service directories, Python's generated `__init__.py` barrel, the two generated accessor files this section's roster is derived from, Rust's generated `accessors.rs`, and Go's hand-written accessors — so identical service sets are enforced rather than merely expected. It reads what each generator already emitted rather than reimplementing the mappings, which is what keeps it from being a seventh copy — with Go the one exception, having no generated per-service files, so its hand-written accessors are compared against the others' generated output and carry the carve-outs noted below. (Python is read from its barrel rather than its directory. That began as a workaround: its generator, alone among the five, did not delete outputs a mapping stopped producing, so a directory listing counted the corpse as still emitted. The generator sweeps now (#757), which fixes it at the source. That sweep reads this same barrel — it is the generator's own record of what it last emitted, and each run deletes `that record minus its own output`, inspecting no file's contents; the two readers share a source and remain independent, since a sweep that stops working leaves the barrel correct and the corpse invisible to a barrel reader exactly as before. The barrel reading is kept for that reason and because it names exactly the modules the mapping produced, excluding the two hand-written base files without a drop-list.) Each per-SDK `check-*-service-drift` script remains the freshness gate for its own SDK; none of them can see another SDK, which is the axis this one adds. Go's three divergences (it folds `automation` and `clientVisibility` into other services and spells `timesheets` singular) are stated as data in that gate and fail it if they ever stop applying; Appendix F records them. +The OpenAPI spec groups operations under coarse tags (e.g., `Card Tables`, `Todos`, `Files`). The service generators split those tags into the `55` fine-grained services above using a two-table mapping: `TAG_TO_SERVICE` (tag → default service name) and `SERVICE_SPLITS` (tag → {service → [operationIds]}). For example, the `Todos` tag splits into `Todos`, `Todolists`, `Todosets`, `TodolistGroups`, `HillCharts`; the `Files` tag splits into `Attachments`, `Uploads`, `Vaults`, `Documents`, `CloudFiles`, `GoogleDocuments`. Both examples are exhaustive on purpose: an abridged one is how `cloudFiles` and `googleDocuments` stayed invisible to this section for so long — a service that arrives through a split rather than a tag of its own is named nowhere a reader would look. These mappings are defined in each language's generator script. They are six hand-maintained copies of one table (Rust's is the `names.toml` its generator reads), and `make check-service-inventory-parity` compares what those copies **emitted** — the TypeScript, Ruby, Kotlin, Swift and Rust generated service directories, Python's generated `__init__.py` barrel, the two generated accessor files this section's roster is derived from, Rust's generated `accessors.rs`, and Go's hand-written accessors — so identical service sets are enforced rather than merely expected. It reads what each generator already emitted rather than reimplementing the mappings, which is what keeps it from being a seventh copy — with Go the one exception, having no generated per-service files, so its hand-written accessors are compared against the others' generated output and carry the carve-outs noted below. (Python is read from its barrel rather than its directory. That began as a workaround: its generator, alone among the five, did not delete outputs a mapping stopped producing, so a directory listing counted the corpse as still emitted. The generator sweeps now (#757), which fixes it at the source. That sweep reads this same barrel — it is the generator's own record of what it last emitted, and each run deletes `that record minus its own output`, inspecting no file's contents; the two readers share a source and remain independent, since a sweep that stops working leaves the barrel correct and the corpse invisible to a barrel reader exactly as before. The barrel reading is kept for that reason and because it names exactly the modules the mapping produced, excluding the two hand-written base files without a drop-list.) Each per-SDK `check-*-service-drift` script remains the freshness gate for its own SDK; none of them can see another SDK, which is the axis this one adds. Go's three divergences (it folds `automation` and `clientVisibility` into other services and spells `timesheets` singular) are stated as data in that gate and fail it if they ever stop applying; Appendix F records them. ### Merge-Safe Write Surface (Cards) From b9a9d06b6ce5f2862a5f453259364f9ac42211d6 Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Fri, 18 Sep 2026 13:11:37 -0700 Subject: [PATCH 3/6] Move ListLineupMarkers onto LineupService, closing the Automation service MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous commit retagged this operation Lineup but carried an explicit 'Lineup' => { 'Automation' => [ListLineupMarkers] } entry in every generator to hold its service still, because moving a public method between services is a break. Taking the break instead. AutomationService held one method and held it by accident. ListLineupMarkers was the only operation the Automation tag reached without a SERVICE_SPLITS entry, so it fell through to the tag-derived service while its three siblings were split into Lineup. Nothing decided that. With no entry carrying it, the retag lands it on Lineup with them and AutomationService stops being emitted: account.automation.list_lineup_markers -> account.lineup.list_lineup_markers in ruby, python, typescript, kotlin, swift and rust. The method name does not change, only the service. Every case is a compile error or a NoMethodError on first call, never a silent wrong answer. MIGRATING.md carries the table. Go is unaffected and is the reason to be confident. Its hand-written surface never had an Automation() accessor — ListLineupMarkers has always been LineupService.ListMarkers there — and that divergence was recorded as a :folded carve-out in check-service-inventory-parity, which synthesized `automation` into Go's inventory so the sets would match. The other six catching up closes the fold for real, so the carve-out goes, along with its rows in SPEC 5.1 and Appendix F. The gate reported it itself the moment the service stopped being generated. That gate's self-test used `automation` as its example of a stale fold carve-out in cases 7 and 15. Those now exercise client_visibility, the surviving one; the mechanism under test is unchanged and only the example moved. The hand-written client wiring moves with the generated services: ruby's Client#automation, python's sync and async accessors, typescript's import, field and defineService call, and typescript's LineupMarker re-export, which now comes from lineup.js. The AutomationService tests fold into the LineupService ones in ruby, typescript and rust rather than being dropped. SPEC section 5 and the Kotlin and Swift READMEs move 55 -> 54 services through doc-constants. Nothing anywhere is named Automation now. --- MIGRATING.md | 31 +++++++++ README.md | 2 +- SPEC.md | 22 +++---- kotlin/README.md | 4 +- .../com/basecamp/sdk/generator/Config.kt | 3 - .../sdk/generated/ServiceAccessors.kt | 4 -- .../sdk/generated/services/automation.kt | 33 ---------- .../basecamp/sdk/generated/services/lineup.kt | 19 ++++++ python/README.md | 1 - python/scripts/generate_services.py | 3 - python/src/basecamp/async_client.py | 6 -- python/src/basecamp/client.py | 6 -- .../basecamp/generated/services/__init__.py | 3 - .../basecamp/generated/services/automation.py | 30 --------- .../src/basecamp/generated/services/lineup.py | 16 +++++ ruby/lib/basecamp/client.rb | 5 -- .../generated/services/automation_service.rb | 19 ------ .../generated/services/lineup_service.rb | 8 +++ ruby/scripts/generate-services.rb | 3 - .../services/automation_service_test.rb | 51 --------------- .../basecamp/services/lineup_service_test.rb | 38 +++++++++++ rust/basecamp-sdk/src/generated/accessors.rs | 5 -- rust/basecamp-sdk/src/generated/routes.rs | 2 +- .../src/generated/services/automation.rs | 40 ------------ .../src/generated/services/lineup.rs | 8 +++ .../src/generated/services/mod.rs | 1 - rust/basecamp-sdk/tests/services.rs | 10 ++- rust/generator/names.toml | 1 - scripts/check-service-inventory-parity | 11 +--- scripts/sync-doc-constants.rb | 2 +- .../test-check-service-inventory-parity.rb | 23 ++++--- spec/overlays/tags.smithy | 18 +++--- swift/README.md | 4 +- .../Generated/AccountClient+Services.swift | 1 - .../Services/AutomationService.swift | 13 ---- .../Generated/Services/LineupService.swift | 9 +++ .../BasecampGenerator/ServiceGrouper.swift | 3 - typescript/scripts/generate-services.ts | 3 - typescript/src/client.ts | 4 -- .../src/generated/services/automation.ts | 49 -------------- typescript/src/generated/services/index.ts | 1 - typescript/src/generated/services/lineup.ts | 26 ++++++++ typescript/src/index.ts | 9 +-- typescript/tests/services/automation.test.ts | 64 ------------------- typescript/tests/services/lineup.test.ts | 42 ++++++++++++ 45 files changed, 245 insertions(+), 411 deletions(-) delete mode 100644 kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/generated/services/automation.kt delete mode 100644 python/src/basecamp/generated/services/automation.py delete mode 100644 ruby/lib/basecamp/generated/services/automation_service.rb delete mode 100644 ruby/test/basecamp/services/automation_service_test.rb delete mode 100644 rust/basecamp-sdk/src/generated/services/automation.rs delete mode 100644 swift/Sources/Basecamp/Generated/Services/AutomationService.swift delete mode 100644 typescript/src/generated/services/automation.ts delete mode 100644 typescript/tests/services/automation.test.ts diff --git a/MIGRATING.md b/MIGRATING.md index f34c5e54f..af90268aa 100644 --- a/MIGRATING.md +++ b/MIGRATING.md @@ -13,6 +13,37 @@ what wrong behaviour you get if you ignore one. This file is that half. # Unreleased +### `listLineupMarkers` moves from the Automation service to the Lineup service + +`AutomationService` is gone. Its only method, the lineup-marker listing, now hangs +off `LineupService` beside `create`, `update` and `delete`, where the other three +lineup-marker operations already were. The method name does not change — only the +service you reach it through. + +| SDK | before | after | +|---|---|---| +| Ruby | `account.automation.list_lineup_markers` | `account.lineup.list_lineup_markers` | +| Python | `account.automation.list_lineup_markers()` | `account.lineup.list_lineup_markers()` | +| TypeScript | `client.automation.listLineupMarkers()` | `client.lineup.listLineupMarkers()` | +| Kotlin | `account.automation.listLineupMarkers()` | `account.lineup.listLineupMarkers()` | +| Swift | `account.automation.listLineupMarkers()` | `account.lineup.listLineupMarkers()` | +| Rust | `account.automation().list_lineup_markers()` | `account.lineup().list_lineup_markers()` | +| Go | `account.Lineup().ListMarkers(...)` | unchanged | + +TypeScript also re-exports the `LineupMarker` type from `lineup.js` rather than +`automation.js`; the type itself is unchanged. `AutomationService` is no longer +exported by any SDK. + +**Loud, not silent.** Every one of these is a compile error, an `AttributeError`, +or a `NoMethodError` on the first call — there is no version of this that silently +returns the wrong thing. Go is unaffected: it never had an `Automation()` accessor +and has always exposed this operation on `LineupService`. + +**Why.** The `Automation` tag was a catch-all over seven unrelated surfaces, and +splitting it into real domains is what the rest of that change does. `AutomationService` +existed only because this one operation fell through the tag's split table while its +three siblings were routed out of it; the tag it was named after no longer exists. + ### Go: the event feed's poll lanes refuse a malformed response instead of typing it `EventFeedService.PollEvents` and `PollInbox` now decide each response against the diff --git a/README.md b/README.md index 5fe6bdfb8..c9259f78f 100644 --- a/README.md +++ b/README.md @@ -250,7 +250,7 @@ All SDKs provide: | **Files** | Vaults, Documents, Uploads, Attachments | | **Card Tables** | CardTables, Cards, CardColumns, CardSteps, Wormholes | | **Client Portal** | ClientApprovals, ClientCorrespondences, ClientReplies | -| **Automation** | Webhooks, Subscriptions, Events | +| **Automation** | Webhooks, Subscriptions, Events, Lineup | | **Reporting** | Search, Reports, Timesheets, Recordings | ## Specification diff --git a/SPEC.md b/SPEC.md index c8ccc19e9..45cb4ecbe 100644 --- a/SPEC.md +++ b/SPEC.md @@ -52,7 +52,7 @@ When artifacts conflict, this precedence governs: |-----------|---------------| | **Config** | Holds validated configuration: base URL, timeouts, retry params, pagination caps. May support env-var override (see §2). | | **Client** | Top-level entry point. Enforces exactly-one-of auth. Owns account-independent services (authorization). | -| **AccountClient** | Account-scoped facade. Prepends `/{accountId}` to paths. Owns all `55` account-scoped services. | +| **AccountClient** | Account-scoped facade. Prepends `/{accountId}` to paths. Owns all `54` account-scoped services. | | **Services** | One class per API resource group. Generated from OpenAPI tags. Methods map to operations. | | **BaseService** | Abstract base for generated services. Provides request execution, error mapping, pagination following, hooks integration. | | **HTTP Transport** | Executes HTTP requests. Applies auth headers, User-Agent, Content-Type. Implements retry, caching. | @@ -295,19 +295,19 @@ Cross-SDK Divergence). - **authorization** — identity lookup and account listing via Launchpad. Exposes `getInfo()` which GETs `https://launchpad.37signals.com/authorization.json` and returns `{expires_at, identity, accounts}`. Implemented in Go, Ruby, and TypeScript. Swift, Kotlin, and Rust do not currently expose this service — a known gap. OAuth utility functions (PKCE, state generation, discovery, code exchange) are standalone helpers in §16, not service methods. -### AccountClient-Level Services (account-scoped) — `55` services +### AccountClient-Level Services (account-scoped) — `54` services -account, attachments, automation, bookmarks, boosts, bubbleUps, calendars, campfires, cardColumns, cardSteps, cardTables, cards, checkins, clientApprovals, clientCorrespondences, clientReplies, clientVisibility, cloudFiles, comments, documents, drafts, eventFeed, events, everything, folders, forwards, gauges, googleDocuments, hillCharts, lineup, messageBoards, messageTypes, messages, myAssignments, myNotes, myNotifications, people, projects, recordings, reports, schedules, search, subscriptions, templates, timeline, timesheets, todolistGroups, todolists, todos, todosets, tools, uploads, vaults, webhooks, wormholes +account, attachments, bookmarks, boosts, bubbleUps, calendars, campfires, cardColumns, cardSteps, cardTables, cards, checkins, clientApprovals, clientCorrespondences, clientReplies, clientVisibility, cloudFiles, comments, documents, drafts, eventFeed, events, everything, folders, forwards, gauges, googleDocuments, hillCharts, lineup, messageBoards, messageTypes, messages, myAssignments, myNotes, myNotifications, people, projects, recordings, reports, schedules, search, subscriptions, templates, timeline, timesheets, todolistGroups, todolists, todos, todosets, tools, uploads, vaults, webhooks, wormholes -**Total surface:** one client-level service (authorization) alongside the `55` account-scoped ones above. +**Total surface:** one client-level service (authorization) alongside the `54` account-scoped ones above. That roster is the canonical surface, not a per-SDK inventory. Accessor counts vary by SDK with split and wiring decisions, and Appendix F tabulates each — that table, not this section, is where a given SDK's surface is stated. ### Derivation Rule `[static]` -The OpenAPI spec groups operations under coarse tags (e.g., `Card Tables`, `Todos`, `Files`). The service generators split those tags into the `55` fine-grained services above using a two-table mapping: `TAG_TO_SERVICE` (tag → default service name) and `SERVICE_SPLITS` (tag → {service → [operationIds]}). For example, the `Todos` tag splits into `Todos`, `Todolists`, `Todosets`, `TodolistGroups`, `HillCharts`; the `Files` tag splits into `Attachments`, `Uploads`, `Vaults`, `Documents`, `CloudFiles`, `GoogleDocuments`. Both examples are exhaustive on purpose: an abridged one is how `cloudFiles` and `googleDocuments` stayed invisible to this section for so long — a service that arrives through a split rather than a tag of its own is named nowhere a reader would look. These mappings are defined in each language's generator script. They are six hand-maintained copies of one table (Rust's is the `names.toml` its generator reads), and `make check-service-inventory-parity` compares what those copies **emitted** — the TypeScript, Ruby, Kotlin, Swift and Rust generated service directories, Python's generated `__init__.py` barrel, the two generated accessor files this section's roster is derived from, Rust's generated `accessors.rs`, and Go's hand-written accessors — so identical service sets are enforced rather than merely expected. It reads what each generator already emitted rather than reimplementing the mappings, which is what keeps it from being a seventh copy — with Go the one exception, having no generated per-service files, so its hand-written accessors are compared against the others' generated output and carry the carve-outs noted below. (Python is read from its barrel rather than its directory. That began as a workaround: its generator, alone among the five, did not delete outputs a mapping stopped producing, so a directory listing counted the corpse as still emitted. The generator sweeps now (#757), which fixes it at the source. That sweep reads this same barrel — it is the generator's own record of what it last emitted, and each run deletes `that record minus its own output`, inspecting no file's contents; the two readers share a source and remain independent, since a sweep that stops working leaves the barrel correct and the corpse invisible to a barrel reader exactly as before. The barrel reading is kept for that reason and because it names exactly the modules the mapping produced, excluding the two hand-written base files without a drop-list.) Each per-SDK `check-*-service-drift` script remains the freshness gate for its own SDK; none of them can see another SDK, which is the axis this one adds. Go's three divergences (it folds `automation` and `clientVisibility` into other services and spells `timesheets` singular) are stated as data in that gate and fail it if they ever stop applying; Appendix F records them. +The OpenAPI spec groups operations under coarse tags (e.g., `Card Tables`, `Todos`, `Files`). The service generators split those tags into the `54` fine-grained services above using a two-table mapping: `TAG_TO_SERVICE` (tag → default service name) and `SERVICE_SPLITS` (tag → {service → [operationIds]}). For example, the `Todos` tag splits into `Todos`, `Todolists`, `Todosets`, `TodolistGroups`, `HillCharts`; the `Files` tag splits into `Attachments`, `Uploads`, `Vaults`, `Documents`, `CloudFiles`, `GoogleDocuments`. Both examples are exhaustive on purpose: an abridged one is how `cloudFiles` and `googleDocuments` stayed invisible to this section for so long — a service that arrives through a split rather than a tag of its own is named nowhere a reader would look. These mappings are defined in each language's generator script. They are six hand-maintained copies of one table (Rust's is the `names.toml` its generator reads), and `make check-service-inventory-parity` compares what those copies **emitted** — the TypeScript, Ruby, Kotlin, Swift and Rust generated service directories, Python's generated `__init__.py` barrel, the two generated accessor files this section's roster is derived from, Rust's generated `accessors.rs`, and Go's hand-written accessors — so identical service sets are enforced rather than merely expected. It reads what each generator already emitted rather than reimplementing the mappings, which is what keeps it from being a seventh copy — with Go the one exception, having no generated per-service files, so its hand-written accessors are compared against the others' generated output and carry the carve-outs noted below. (Python is read from its barrel rather than its directory. That began as a workaround: its generator, alone among the five, did not delete outputs a mapping stopped producing, so a directory listing counted the corpse as still emitted. The generator sweeps now (#757), which fixes it at the source. That sweep reads this same barrel — it is the generator's own record of what it last emitted, and each run deletes `that record minus its own output`, inspecting no file's contents; the two readers share a source and remain independent, since a sweep that stops working leaves the barrel correct and the corpse invisible to a barrel reader exactly as before. The barrel reading is kept for that reason and because it names exactly the modules the mapping produced, excluding the two hand-written base files without a drop-list.) Each per-SDK `check-*-service-drift` script remains the freshness gate for its own SDK; none of them can see another SDK, which is the axis this one adds. Go's two divergences (it folds `clientVisibility` into another service and spells `timesheets` singular) are stated as data in that gate and fail it if they ever stop applying; Appendix F records them. ### Merge-Safe Write Surface (Cards) @@ -509,7 +509,7 @@ Conformance: `conformance/tests/schedule_entries_write.json`, 11 cases. Nine cov ### Known Gaps (informational, not prescriptive) -- Go is missing a standalone `automation` service; `clientVisibility` is implemented on `RecordingsService` (not a separate service); uses singular `Timesheet` vs `timesheets` +- Go implements `clientVisibility` on `RecordingsService` (not a separate service); uses singular `Timesheet` vs `timesheets` - TypeScript flattens both tiers onto a single client object (no separate AccountClient exposed to consumers) — a valid language adaptation - Ruby returns a lazy `ListEnumerator` (an `Enumerator` subclass carrying `ListMeta`) for pagination rather than an eager `ListResult`; the first page is fetched eagerly so `meta.total_count` is available at call time, while `meta.truncated` is final only once enumeration completes @@ -5010,9 +5010,9 @@ Repeated from §5 for quick reference. **Client-level (1):** authorization -**AccountClient-level (`55`):** +**AccountClient-level (`54`):** -account, attachments, automation, bookmarks, boosts, bubbleUps, calendars, campfires, cardColumns, cardSteps, cardTables, cards, checkins, clientApprovals, clientCorrespondences, clientReplies, clientVisibility, cloudFiles, comments, documents, drafts, eventFeed, events, everything, folders, forwards, gauges, googleDocuments, hillCharts, lineup, messageBoards, messageTypes, messages, myAssignments, myNotes, myNotifications, people, projects, recordings, reports, schedules, search, subscriptions, templates, timeline, timesheets, todolistGroups, todolists, todos, todosets, tools, uploads, vaults, webhooks, wormholes +account, attachments, bookmarks, boosts, bubbleUps, calendars, campfires, cardColumns, cardSteps, cardTables, cards, checkins, clientApprovals, clientCorrespondences, clientReplies, clientVisibility, cloudFiles, comments, documents, drafts, eventFeed, events, everything, folders, forwards, gauges, googleDocuments, hillCharts, lineup, messageBoards, messageTypes, messages, myAssignments, myNotes, myNotifications, people, projects, recordings, reports, schedules, search, subscriptions, templates, timeline, timesheets, todolistGroups, todolists, todos, todosets, tools, uploads, vaults, webhooks, wormholes --- @@ -5508,11 +5508,11 @@ SDK's own hand-written file. | SDK | Account-scoped services | |-----|------------------------| -| Swift | `55` — full canonical set (`AccountClient+Services.swift`, generated; one of §5's two sources) | -| Kotlin | `55` — full canonical set (`ServiceAccessors.kt`, generated; §5's other source). Eight accessors expose handwritten composites that subclass their generated service — `cards`, `comments`, `documents`, `recordings`, `schedules`, `todolists`, `todos`, `uploads`, per the generator's `HAND_WRITTEN_SERVICES` — and the rest are the generated classes directly. The accessor set is identical either way, which is why §5 derives from this file regardless | +| Swift | `54` — full canonical set (`AccountClient+Services.swift`, generated; one of §5's two sources) | +| Kotlin | `54` — full canonical set (`ServiceAccessors.kt`, generated; §5's other source). Eight accessors expose handwritten composites that subclass their generated service — `cards`, `comments`, `documents`, `recordings`, `schedules`, `todolists`, `todos`, `uploads`, per the generator's `HAND_WRITTEN_SERVICES` — and the rest are the generated classes directly. The accessor set is identical either way, which is why §5 derives from this file regardless | | Ruby | 55 — full canonical set. Held by its own accessor-roster test (`ruby/test/basecamp/accessor_inventory_test.rb`, added in #755) deriving the roster from `lib/basecamp/generated/services/`, so the next unwired service fails rather than going unnoticed. The seven hand-written composites are `prepend`ed onto their generated classes rather than subclassing them, so every accessor's class is the generated constant exactly — the five merge-safe writes, plus `recordings` and `comments` for the recording-summary and mention helpers (Appendix F), which is the count `ruby/lib/basecamp.rb`'s `on_load` hooks hold. | | TypeScript | 55 — full canonical set, on the flat client alongside `authorization` (no `AccountClient` tier; see Client Topology above). Held by its own accessor-roster tests (`typescript/tests/accessor-inventory.test.ts` and `tests/types/accessor-inventory.test-d.ts`, added in #755) deriving the roster from `src/generated/services/`. Four hand-maintained renderings, so two instruments: the imports and `defineService` calls are resolved on a constructed client, the `index.ts` export blocks get their own assertion (a missing export is invisible at runtime to an in-repo importer and only bites a consumer), and the `BasecampClient` interface is asserted type-level, the factory returning `client as BasecampClient` so no runtime check can see it. Eight accessors expose hand-written composites that subclass their generated service, which the class assertions allow for: `todos`, `todolists`, `cards`, `documents`, `uploads`, `schedules`, and — since the recording-summary port — `comments` and `recordings`. Named rather than counted, because a count is stale the moment a composite is added and nobody re-reads an inventory row to notice. | -| Go | 53 accessors. Two services are folded rather than missing: `automation`'s sole operation is `LineupService.ListMarkers`, and `clientVisibility`'s is `RecordingsService.SetClientVisibility`. `timesheets` is spelled `Timesheet` (singular). Capability is 55/55; the surface is not. Hand-written service wrappers around the generated OpenAPI client — not fully generated. | +| Go | 53 accessors. One service is folded rather than missing: `clientVisibility`'s sole operation is `RecordingsService.SetClientVisibility`. `timesheets` is spelled `Timesheet` (singular). Capability is 54/54; the surface is not. Hand-written service wrappers around the generated OpenAPI client — not fully generated. | | Python | 55 — full canonical set. `gauges` and `my_notifications` were a wiring gap rather than a fold, and were wired in #732; the same change added an accessor-inventory test (`python/tests/test_client.py`) deriving its roster from `generated/services/`, so the next unwired service fails rather than going unnoticed. Sync and async agree exactly. | | Rust | 55 — full canonical set; `accessors.rs`, generated, read by `make check-service-inventory-parity` | diff --git a/kotlin/README.md b/kotlin/README.md index 14b7a5b08..3dc11341f 100644 --- a/kotlin/README.md +++ b/kotlin/README.md @@ -12,7 +12,7 @@ Official Kotlin SDK for the [Basecamp API](https://github.com/basecamp/bc3-api). - Kotlin Multiplatform (JVM target) - Builder DSL for client configuration -- `55` services covering the complete Basecamp API +- `54` services covering the complete Basecamp API - OAuth 2.0 with PKCE support - Webhook signature verification (HMAC-SHA256) - ETag-based HTTP caching (opt-in) @@ -486,7 +486,7 @@ if (!isValid) { ## Services -The SDK exposes `55` account-scoped services. The tables below group the common ones; see `com/basecamp/sdk/generated/services/` for the full set. +The SDK exposes `54` account-scoped services. The tables below group the common ones; see `com/basecamp/sdk/generated/services/` for the full set. ### Projects & Organization diff --git a/kotlin/generator/src/main/kotlin/com/basecamp/sdk/generator/Config.kt b/kotlin/generator/src/main/kotlin/com/basecamp/sdk/generator/Config.kt index 0c7728b2f..5b189ce48 100644 --- a/kotlin/generator/src/main/kotlin/com/basecamp/sdk/generator/Config.kt +++ b/kotlin/generator/src/main/kotlin/com/basecamp/sdk/generator/Config.kt @@ -57,9 +57,6 @@ val SERVICE_SPLITS: Map>> = mapOf( "Dock" to mapOf( "Tools" to listOf("GetTool", "UpdateTool", "DeleteTool", "CreateTool", "EnableTool", "DisableTool", "RepositionTool"), ), - "Lineup" to mapOf( - "Automation" to listOf("ListLineupMarkers"), - ), "Recordings" to mapOf( "Events" to listOf("ListEvents"), ), diff --git a/kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/generated/ServiceAccessors.kt b/kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/generated/ServiceAccessors.kt index c850726cb..4aab63ae7 100644 --- a/kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/generated/ServiceAccessors.kt +++ b/kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/generated/ServiceAccessors.kt @@ -19,10 +19,6 @@ val AccountClient.account: AccountService val AccountClient.attachments: AttachmentsService get() = service("Attachments") { AttachmentsService(this) } -/** Automation operations. */ -val AccountClient.automation: AutomationService - get() = service("Automation") { AutomationService(this) } - /** Bookmarks operations. */ val AccountClient.bookmarks: BookmarksService get() = service("Bookmarks") { BookmarksService(this) } diff --git a/kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/generated/services/automation.kt b/kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/generated/services/automation.kt deleted file mode 100644 index bc236b35c..000000000 --- a/kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/generated/services/automation.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.basecamp.sdk.generated.services - -import com.basecamp.sdk.* -import com.basecamp.sdk.generated.models.* -import com.basecamp.sdk.services.BaseService -import kotlinx.serialization.json.JsonElement - -/** - * Service for Automation operations. - * - * @generated from OpenAPI spec — do not edit directly - */ -class AutomationService(client: AccountClient) : BaseService(client) { - - /** - * List all lineup markers for the account - */ - suspend fun listLineupMarkers(): List { - val info = OperationInfo( - service = "Automation", - operation = "ListLineupMarkers", - resourceType = "lineup_marker", - isMutation = false, - projectId = null, - resourceId = null, - ) - return request(info, { - httpGet("/lineup/markers.json", operationName = info.operation) - }) { body -> - json.decodeFromString>(body) - } - } -} diff --git a/kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/generated/services/lineup.kt b/kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/generated/services/lineup.kt index 9262110ce..482d3d860 100644 --- a/kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/generated/services/lineup.kt +++ b/kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/generated/services/lineup.kt @@ -12,6 +12,25 @@ import kotlinx.serialization.json.JsonElement */ class LineupService(client: AccountClient) : BaseService(client) { + /** + * List all lineup markers for the account + */ + suspend fun listLineupMarkers(): List { + val info = OperationInfo( + service = "Lineup", + operation = "ListLineupMarkers", + resourceType = "lineup_marker", + isMutation = false, + projectId = null, + resourceId = null, + ) + return request(info, { + httpGet("/lineup/markers.json", operationName = info.operation) + }) { body -> + json.decodeFromString>(body) + } + } + /** * Create a new lineup marker * @param body Request body diff --git a/python/README.md b/python/README.md index 167dfa181..da1bf335b 100644 --- a/python/README.md +++ b/python/README.md @@ -493,7 +493,6 @@ All services are constructed from an `AccountClient`, obtained via `client.for_a | **Automation** | Webhooks | `account.webhooks` | | | Subscriptions | `account.subscriptions` | | | Events | `account.events` | -| | Automation | `account.automation` | | | Boosts | `account.boosts` | | **Reporting** | Search | `account.search` | | | Reports | `account.reports` | diff --git a/python/scripts/generate_services.py b/python/scripts/generate_services.py index c52580397..c49923b46 100644 --- a/python/scripts/generate_services.py +++ b/python/scripts/generate_services.py @@ -83,9 +83,6 @@ "Dock": { "Tools": ["GetTool", "UpdateTool", "DeleteTool", "CreateTool", "EnableTool", "DisableTool", "RepositionTool"], }, - "Lineup": { - "Automation": ["ListLineupMarkers"], - }, "Recordings": { "Events": ["ListEvents"], }, diff --git a/python/src/basecamp/async_client.py b/python/src/basecamp/async_client.py index fd1c4830c..b49cf2c06 100644 --- a/python/src/basecamp/async_client.py +++ b/python/src/basecamp/async_client.py @@ -407,12 +407,6 @@ def lineup(self): return self._service("lineup", lambda: AsyncLineupService(self)) - @property - def automation(self): - from basecamp.generated.services.automation import AsyncAutomationService - - return self._service("automation", lambda: AsyncAutomationService(self)) - @property def bookmarks(self): from basecamp.generated.services.bookmarks import AsyncBookmarksService diff --git a/python/src/basecamp/client.py b/python/src/basecamp/client.py index 4b7ede471..3625323d9 100644 --- a/python/src/basecamp/client.py +++ b/python/src/basecamp/client.py @@ -408,12 +408,6 @@ def lineup(self): return self._service("lineup", lambda: LineupService(self)) - @property - def automation(self): - from basecamp.generated.services.automation import AutomationService - - return self._service("automation", lambda: AutomationService(self)) - @property def bookmarks(self): from basecamp.generated.services.bookmarks import BookmarksService diff --git a/python/src/basecamp/generated/services/__init__.py b/python/src/basecamp/generated/services/__init__.py index dc744d917..d43b706d7 100644 --- a/python/src/basecamp/generated/services/__init__.py +++ b/python/src/basecamp/generated/services/__init__.py @@ -2,7 +2,6 @@ from basecamp.generated.services.account import AccountService, AsyncAccountService from basecamp.generated.services.attachments import AttachmentsService, AsyncAttachmentsService -from basecamp.generated.services.automation import AutomationService, AsyncAutomationService from basecamp.generated.services.bookmarks import BookmarksService, AsyncBookmarksService from basecamp.generated.services.boosts import BoostsService, AsyncBoostsService from basecamp.generated.services.bubble_ups import BubbleUpsService, AsyncBubbleUpsService @@ -64,8 +63,6 @@ "AsyncAccountService", "AttachmentsService", "AsyncAttachmentsService", - "AutomationService", - "AsyncAutomationService", "BookmarksService", "AsyncBookmarksService", "BoostsService", diff --git a/python/src/basecamp/generated/services/automation.py b/python/src/basecamp/generated/services/automation.py deleted file mode 100644 index 97a51f508..000000000 --- a/python/src/basecamp/generated/services/automation.py +++ /dev/null @@ -1,30 +0,0 @@ -# @generated from OpenAPI spec — do not edit manually - -from __future__ import annotations - -from typing import Any - -from basecamp.generated.services._base import BaseService -from basecamp.generated.services._async_base import AsyncBaseService -from basecamp._pagination import ListResult -from basecamp.hooks import OperationInfo - - -class AutomationService(BaseService): - def list_lineup_markers(self) -> ListResult: - """List all lineup markers for the account.""" - return self._request_list( - OperationInfo(service="automation", operation="list_lineup_markers", is_mutation=False), - "/lineup/markers.json", - operation="ListLineupMarkers", - ) - - -class AsyncAutomationService(AsyncBaseService): - async def list_lineup_markers(self) -> ListResult: - """List all lineup markers for the account.""" - return await self._request_list( - OperationInfo(service="automation", operation="list_lineup_markers", is_mutation=False), - "/lineup/markers.json", - operation="ListLineupMarkers", - ) diff --git a/python/src/basecamp/generated/services/lineup.py b/python/src/basecamp/generated/services/lineup.py index fd3d1d8e0..99433b444 100644 --- a/python/src/basecamp/generated/services/lineup.py +++ b/python/src/basecamp/generated/services/lineup.py @@ -11,6 +11,14 @@ class LineupService(BaseService): + def list_lineup_markers(self) -> ListResult: + """List all lineup markers for the account.""" + return self._request_list( + OperationInfo(service="lineup", operation="list_lineup_markers", is_mutation=False), + "/lineup/markers.json", + operation="ListLineupMarkers", + ) + def create(self, *, name: str, date: str) -> None: """Create a new lineup marker. @@ -57,6 +65,14 @@ def delete(self, *, marker_id: int) -> None: class AsyncLineupService(AsyncBaseService): + async def list_lineup_markers(self) -> ListResult: + """List all lineup markers for the account.""" + return await self._request_list( + OperationInfo(service="lineup", operation="list_lineup_markers", is_mutation=False), + "/lineup/markers.json", + operation="ListLineupMarkers", + ) + async def create(self, *, name: str, date: str) -> None: """Create a new lineup marker. diff --git a/ruby/lib/basecamp/client.rb b/ruby/lib/basecamp/client.rb index d0ab07eb4..ecddfb78e 100644 --- a/ruby/lib/basecamp/client.rb +++ b/ruby/lib/basecamp/client.rb @@ -516,11 +516,6 @@ def lineup service(:lineup) { Services::LineupService.new(self) } end - # @return [Services::AutomationService] - def automation - service(:automation) { Services::AutomationService.new(self) } - end - # @return [Services::MessageTypesService] def message_types service(:message_types) { Services::MessageTypesService.new(self) } diff --git a/ruby/lib/basecamp/generated/services/automation_service.rb b/ruby/lib/basecamp/generated/services/automation_service.rb deleted file mode 100644 index 1aca6eea0..000000000 --- a/ruby/lib/basecamp/generated/services/automation_service.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -module Basecamp - module Services - # Service for Automation operations - # - # @generated from OpenAPI spec - class AutomationService < BaseService - - # List all lineup markers for the account - # @return [Array] response data - def list_lineup_markers() - with_operation(service: "automation", operation: "list_lineup_markers", is_mutation: false) do - http_get("/lineup/markers.json", operation: "ListLineupMarkers").json(operation: "ListLineupMarkers") - end - end - end - end -end diff --git a/ruby/lib/basecamp/generated/services/lineup_service.rb b/ruby/lib/basecamp/generated/services/lineup_service.rb index b9fdb589d..3a7a01e5b 100644 --- a/ruby/lib/basecamp/generated/services/lineup_service.rb +++ b/ruby/lib/basecamp/generated/services/lineup_service.rb @@ -7,6 +7,14 @@ module Services # @generated from OpenAPI spec class LineupService < BaseService + # List all lineup markers for the account + # @return [Array] response data + def list_lineup_markers() + with_operation(service: "lineup", operation: "list_lineup_markers", is_mutation: false) do + http_get("/lineup/markers.json", operation: "ListLineupMarkers").json(operation: "ListLineupMarkers") + end + end + # Create a new lineup marker # @param name [String] name # @param date [String] date diff --git a/ruby/scripts/generate-services.rb b/ruby/scripts/generate-services.rb index bde30a3c1..d2bc6ff93 100644 --- a/ruby/scripts/generate-services.rb +++ b/ruby/scripts/generate-services.rb @@ -110,9 +110,6 @@ class ServiceGenerator 'Dock' => { 'Tools' => %w[GetTool UpdateTool DeleteTool CreateTool EnableTool DisableTool RepositionTool] }, - 'Lineup' => { - 'Automation' => %w[ListLineupMarkers] - }, 'Recordings' => { 'Events' => %w[ListEvents] }, diff --git a/ruby/test/basecamp/services/automation_service_test.rb b/ruby/test/basecamp/services/automation_service_test.rb deleted file mode 100644 index 65cb79d5c..000000000 --- a/ruby/test/basecamp/services/automation_service_test.rb +++ /dev/null @@ -1,51 +0,0 @@ -# frozen_string_literal: true - -# Tests for the AutomationService (generated from OpenAPI spec) - -require "test_helper" - -class AutomationServiceTest < Minitest::Test - include TestHelper - - def setup - @account = create_account_client(account_id: "12345") - end - - def test_list_lineup_markers - markers = [ - { - id: 1069479400, - name: "Product Launch", - date: "2024-03-01", - created_at: "2024-02-15T10:30:00.000Z", - updated_at: "2024-02-15T10:30:00.000Z" - }, - { - id: 1069479401, - name: "Quarterly Review", - date: "2024-06-15", - created_at: "2024-03-01T09:00:00.000Z", - updated_at: "2024-03-01T09:00:00.000Z" - } - ] - - stub_get("/12345/lineup/markers.json", response_body: markers) - - result = @account.automation.list_lineup_markers.to_a - - assert_kind_of Array, result - assert_equal 2, result.length - assert_equal 1069479400, result[0]["id"] - assert_equal "Product Launch", result[0]["name"] - assert_equal "2024-03-01", result[0]["date"] - end - - def test_list_lineup_markers_empty - stub_get("/12345/lineup/markers.json", response_body: []) - - result = @account.automation.list_lineup_markers.to_a - - assert_kind_of Array, result - assert_equal 0, result.length - end -end diff --git a/ruby/test/basecamp/services/lineup_service_test.rb b/ruby/test/basecamp/services/lineup_service_test.rb index ac0f9423b..87182554c 100644 --- a/ruby/test/basecamp/services/lineup_service_test.rb +++ b/ruby/test/basecamp/services/lineup_service_test.rb @@ -15,6 +15,44 @@ def setup @account = create_account_client(account_id: "12345") end + def test_list_lineup_markers + markers = [ + { + id: 1069479400, + name: "Product Launch", + date: "2024-03-01", + created_at: "2024-02-15T10:30:00.000Z", + updated_at: "2024-02-15T10:30:00.000Z" + }, + { + id: 1069479401, + name: "Quarterly Review", + date: "2024-06-15", + created_at: "2024-03-01T09:00:00.000Z", + updated_at: "2024-03-01T09:00:00.000Z" + } + ] + + stub_get("/12345/lineup/markers.json", response_body: markers) + + result = @account.lineup.list_lineup_markers.to_a + + assert_kind_of Array, result + assert_equal 2, result.length + assert_equal 1069479400, result[0]["id"] + assert_equal "Product Launch", result[0]["name"] + assert_equal "2024-03-01", result[0]["date"] + end + + def test_list_lineup_markers_empty + stub_get("/12345/lineup/markers.json", response_body: []) + + result = @account.lineup.list_lineup_markers.to_a + + assert_kind_of Array, result + assert_equal 0, result.length + end + def test_create stub_post("/12345/lineup/markers.json", response_body: "", status: 201) diff --git a/rust/basecamp-sdk/src/generated/accessors.rs b/rust/basecamp-sdk/src/generated/accessors.rs index 89b44f940..c50b25c03 100644 --- a/rust/basecamp-sdk/src/generated/accessors.rs +++ b/rust/basecamp-sdk/src/generated/accessors.rs @@ -17,11 +17,6 @@ impl AccountClient { services::attachments::AttachmentsService::new(self) } - /// `Automation` operations. - pub fn automation(&self) -> services::automation::AutomationService<'_> { - services::automation::AutomationService::new(self) - } - /// `Bookmarks` operations. pub fn bookmarks(&self) -> services::bookmarks::BookmarksService<'_> { services::bookmarks::BookmarksService::new(self) diff --git a/rust/basecamp-sdk/src/generated/routes.rs b/rust/basecamp-sdk/src/generated/routes.rs index c1305ec20..3cf6e6b80 100644 --- a/rust/basecamp-sdk/src/generated/routes.rs +++ b/rust/basecamp-sdk/src/generated/routes.rs @@ -3530,7 +3530,7 @@ pub static LIST_GAUGES: Route = Route { /// `GET /lineup/markers.json`. pub static LIST_LINEUP_MARKERS: Route = Route { id: "ListLineupMarkers", - service: "Automation", + service: "Lineup", method: Method::GET, path: "/lineup/markers.json", pattern: "/lineup/markers", diff --git a/rust/basecamp-sdk/src/generated/services/automation.rs b/rust/basecamp-sdk/src/generated/services/automation.rs deleted file mode 100644 index e1a5fb653..000000000 --- a/rust/basecamp-sdk/src/generated/services/automation.rs +++ /dev/null @@ -1,40 +0,0 @@ -// Generated by basecamp-sdk-generator from openapi.json and behavior-model.json. DO NOT EDIT. - -//! Automation operations. - -#![allow( - clippy::too_many_arguments, - clippy::wildcard_imports, - clippy::doc_markdown -)] - -use crate::client::AccountClient; -use crate::error::Error; -use crate::generated::routes; -#[allow(unused_imports)] -use crate::generated::types::*; - -/// `Automation` operations, sent through one [`AccountClient`]. -#[derive(Debug, Clone, Copy)] -pub struct AutomationService<'a> { - client: &'a AccountClient, -} - -impl<'a> AutomationService<'a> { - pub(crate) fn new(client: &'a AccountClient) -> Self { - Self { client } - } - - /// The client this service sends through. - pub fn client(&self) -> &'a AccountClient { - self.client - } - - /// List all lineup markers for the account - /// - /// `GET /lineup/markers.json` — idempotent; retries up to 3 attempt(s) on 429, 503. - pub async fn list_lineup_markers(&self) -> Result { - let operation = self.client.operation(&routes::LIST_LINEUP_MARKERS, &[]); - self.client.send(operation).await - } -} diff --git a/rust/basecamp-sdk/src/generated/services/lineup.rs b/rust/basecamp-sdk/src/generated/services/lineup.rs index fa4428f51..3be60653a 100644 --- a/rust/basecamp-sdk/src/generated/services/lineup.rs +++ b/rust/basecamp-sdk/src/generated/services/lineup.rs @@ -49,6 +49,14 @@ impl<'a> LineupService<'a> { self.client.send_unit(operation).await } + /// List all lineup markers for the account + /// + /// `GET /lineup/markers.json` — idempotent; retries up to 3 attempt(s) on 429, 503. + pub async fn list_lineup_markers(&self) -> Result { + let operation = self.client.operation(&routes::LIST_LINEUP_MARKERS, &[]); + self.client.send(operation).await + } + /// Update an existing lineup marker /// /// `PUT /lineup/markers/{markerId}` — idempotent; retries up to 3 attempt(s) on 429, 503. diff --git a/rust/basecamp-sdk/src/generated/services/mod.rs b/rust/basecamp-sdk/src/generated/services/mod.rs index 98d27b7e4..6658b45fe 100644 --- a/rust/basecamp-sdk/src/generated/services/mod.rs +++ b/rust/basecamp-sdk/src/generated/services/mod.rs @@ -4,7 +4,6 @@ pub mod account; pub mod attachments; -pub mod automation; pub mod bookmarks; pub mod boosts; pub mod bubble_ups; diff --git a/rust/basecamp-sdk/tests/services.rs b/rust/basecamp-sdk/tests/services.rs index 51fce7f34..b05b1dccc 100644 --- a/rust/basecamp-sdk/tests/services.rs +++ b/rust/basecamp-sdk/tests/services.rs @@ -53,7 +53,7 @@ async fn attachments_create_reaches_the_wire() { } #[tokio::test] -async fn automation_list_lineup_markers_reaches_the_wire() { +async fn lineup_list_lineup_markers_reaches_the_wire() { let server = MockServer::start().await; Mock::given(method("GET")) .and(path("/999/lineup/markers.json")) @@ -68,7 +68,7 @@ async fn automation_list_lineup_markers_reaches_the_wire() { .mount(&server) .await; account(&server) - .automation() + .lineup() .list_lineup_markers() .await .unwrap(); @@ -1210,10 +1210,8 @@ error_case!( ); error_case!( - automation_list_lineup_markers_maps_a_rejection, - |account: basecamp_sdk::AccountClient| async move { - account.automation().list_lineup_markers().await - } + lineup_list_lineup_markers_maps_a_rejection, + |account: basecamp_sdk::AccountClient| async move { account.lineup().list_lineup_markers().await } ); error_case!( diff --git a/rust/generator/names.toml b/rust/generator/names.toml index c78e0edd4..241576013 100644 --- a/rust/generator/names.toml +++ b/rust/generator/names.toml @@ -29,7 +29,6 @@ # says. Ordered by service, then operation, so a diff against Config.kt reads in blocks. [operation_services] CreateAttachment = "Attachments" -ListLineupMarkers = "Automation" CreateCampfireLine = "Campfires" CreateCampfireUpload = "Campfires" CreateChatbot = "Campfires" diff --git a/scripts/check-service-inventory-parity b/scripts/check-service-inventory-parity index ef8d05e3c..1256efd23 100755 --- a/scripts/check-service-inventory-parity +++ b/scripts/check-service-inventory-parity @@ -36,7 +36,7 @@ # go/pkg/basecamp/client.go — a hand-written surface compared against nine # generated ones. That is not a flaw in the comparison (Go's accessors are still # an independent statement of which services exist) but it is why Go, alone, -# needs three recorded carve-outs, and why it gets the accessor-vs-returned-type +# needs its recorded carve-outs, and why it gets the accessor-vs-returned-type # check that a generated file would not need. # # WHAT IT CLOSES. `make doc-constants-check` derives SPEC §5's account-scoped @@ -56,7 +56,7 @@ # NORMALIZATION IS DATA, NOT BRANCHES. Each SDK spells the same service its own # way, and the entire rule set for a source is the fields on its SOURCES entry: # which files to drop, one suffix to strip, one identifier casing. Go additionally -# carries three named carve-outs. Nothing here is a per-SDK code path, because the +# carries two named carve-outs. Nothing here is a per-SDK code path, because the # failure mode this gate exists to catch is exactly what accumulating per-SDK # special cases produces. If a new SDK needs a rule that is not one of these # fields, that is a finding about the generator's naming, not a place to add an @@ -240,7 +240,7 @@ SOURCES = [ }, ].freeze -# Go's three divergences from the canonical set, recorded in SPEC Appendix F. +# Go's two divergences from the canonical set, recorded in SPEC Appendix F. # They are stated here as data for the same reason the spelling rules are: a # carve-out expressed as a branch is indistinguishable from a bug, and a # carve-out nobody re-reads is how a surface silently stays behind. @@ -251,11 +251,6 @@ SOURCES = [ # leaving a rule that quietly stops meaning anything. Same device as # spec/doc-constants.json's committed marker counts. GO_CARVE_OUTS = [ - { - kind: :folded, - name: "automation", - reason: "its sole operation, ListMarkers, is exposed on LineupService", - }, { kind: :folded, name: "client_visibility", diff --git a/scripts/sync-doc-constants.rb b/scripts/sync-doc-constants.rb index 70d8090b1..672d3f656 100755 --- a/scripts/sync-doc-constants.rb +++ b/scripts/sync-doc-constants.rb @@ -366,7 +366,7 @@ def tracked_fixtures # rather than leaving it to be inferred: their entire reason to exist is hanging # services off AccountClient, which is exactly what §5's roster claims. Per-SDK # client wiring cannot answer the question — it drifts by design (Go folds -# `automation` and `clientVisibility` and spells `timesheets` singular; Python is +# `clientVisibility` and spells `timesheets` singular; Python is # two short; TypeScript has no account-scoped tier at all), so a roster derived # from any one of them would assert that SDK's gaps as the canonical surface. KOTLIN_ACCESSORS = "kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/generated/ServiceAccessors.kt" diff --git a/scripts/test-check-service-inventory-parity.rb b/scripts/test-check-service-inventory-parity.rb index 96ec0fb74..1cca4931e 100644 --- a/scripts/test-check-service-inventory-parity.rb +++ b/scripts/test-check-service-inventory-parity.rb @@ -14,7 +14,7 @@ # # THE SYNTHETIC TREE IS ITSELF UNDER TEST. It is built by INVERTING each source's # normalization — snake_case back to kebab, camelCase, PascalCase, `_service` -# suffixes, Rust's `Service<'_>` accessors, Go's three carve-outs — from +# suffixes, Rust's `Service<'_>` accessors, Go's carve-outs — from # one canonical list read out of the real Kotlin accessors. A builder that # spelled anything differently from the real # generators would make every negative case below fail for a reason unrelated to @@ -178,7 +178,7 @@ def rs_accessor(name) # Go's carve-outs, inverted: the two folded services have no accessor at all, and # `timesheets` is spelled singular. -GO_FOLDED = %w[automation client_visibility].freeze +GO_FOLDED = %w[client_visibility].freeze def go_accessor_names(names) names.reject { |n| GO_FOLDED.include?(n) }.map { |n| n == "timesheets" ? "timesheet" : n } @@ -397,13 +397,16 @@ def expect_fail_without(failures, label, out, status, fragment, forbidden) # --- 7. A fold carve-out that closed ------------------------------------------- # -# Go grows an Automation() accessor. Without the staleness check this is INVISIBLE -# — the carve-out adds `automation` to Go's set either way, so the parity diff is -# empty and SPEC Appendix F keeps claiming a fold that no longer exists. +# Go grows a ClientVisibility() accessor. Without the staleness check this is +# INVISIBLE — the carve-out adds `client_visibility` to Go's set either way, so +# the parity diff is empty and SPEC Appendix F keeps claiming a fold that no +# longer exists. (This case named `automation` until #930 closed that fold for +# real by moving ListLineupMarkers onto LineupService in the other six SDKs, +# which is what Go had done all along.) -out, status = with_root(names: { "go-accessors" => go_accessor_names(CANONICAL) + ["automation"] }) -expect_fail(failures, "7. Go closed the automation fold, carve-out now stale", out, status, - "now exposes `automation`, but it is recorded as a fold carve-out") +out, status = with_root(names: { "go-accessors" => go_accessor_names(CANONICAL) + ["client_visibility"] }) +expect_fail(failures, "7. Go closed the clientVisibility fold, carve-out now stale", out, status, + "now exposes `client_visibility`, but it is recorded as a fold carve-out") # --- 8. A spelling carve-out that closed --------------------------------------- @@ -413,7 +416,7 @@ def expect_fail_without(failures, label, out, status, fragment, forbidden) # --- 9. Go short a service its carve-outs do not cover -------------------------- # -# The carve-outs are three named divergences, not a blanket exemption. A fourth +# The carve-outs are two named divergences, not a blanket exemption. A third # service missing from Go has to fail like any other SDK's would. out, status = with_root(names: { "go-accessors" => go_accessor_names(CANONICAL) - ["gauges"] }) @@ -511,7 +514,7 @@ def expect_fail_without(failures, label, out, status, fragment, forbidden) # Pinned as an ABSENCE, which is the only way to hold a diagnostic honest: the # run must fail, name the carve-out, and NOT say a tag mapping drifted. -out, status = with_root(names: { "go-accessors" => go_accessor_names(CANONICAL) + ["automation"] }) +out, status = with_root(names: { "go-accessors" => go_accessor_names(CANONICAL) + ["client_visibility"] }) expect_fail_without(failures, "15. stale carve-out failure does not misdiagnose a tag mapping", out, status, "recorded as a fold carve-out", diff --git a/spec/overlays/tags.smithy b/spec/overlays/tags.smithy index 2b7579a3b..07c4e7c75 100644 --- a/spec/overlays/tags.smithy +++ b/spec/overlays/tags.smithy @@ -226,16 +226,14 @@ apply RepositionTool @tags(["Dock"]) // Lineup (account-wide schedule markers) // -// ListLineupMarkers is the one operation here whose SERVICE is not Lineup. It -// has always generated into AutomationService — it was the only operation the -// Automation tag reached without a SERVICE_SPLITS entry, so it fell through to -// the tag-derived service while its three siblings were split out. Retagging it -// Lineup without saying so would move a public method between services, so each -// tag-keyed generator now carries 'Lineup' => { 'Automation' => [...] } and the -// Rust table an explicit ListLineupMarkers = "Automation" row. The tag is gone; -// the service it named survives, holding exactly this one method, as it did -// before. Merging it into LineupService is a breaking change and a separate -// decision. +// ListLineupMarkers moves SERVICE as well as tag, and it is the only operation +// in this split that does. It was the one operation the Automation tag reached +// without a SERVICE_SPLITS entry, so it fell through to the tag-derived service +// and generated into AutomationService alone while its three siblings were +// split into Lineup. With no entry carrying it, retagging lands it on Lineup +// with them, AutomationService stops being emitted, and every SDK's +// AutomationService#list_lineup_markers becomes LineupService#list. That is a +// breaking change, taken deliberately and recorded in MIGRATING.md. apply ListLineupMarkers @tags(["Lineup"]) apply CreateLineupMarker @tags(["Lineup"]) apply UpdateLineupMarker @tags(["Lineup"]) diff --git a/swift/README.md b/swift/README.md index 9a69b3fd3..7ad8b8f37 100644 --- a/swift/README.md +++ b/swift/README.md @@ -11,7 +11,7 @@ Official Swift SDK for the [Basecamp API](https://github.com/basecamp/bc3-api). ## Features - Full Swift 6 concurrency support (strict `Sendable` throughout) -- `55` services covering the complete Basecamp API +- `54` services covering the complete Basecamp API - Async/await API with structured concurrency - ETag-based HTTP caching (opt-in) - Automatic retry with exponential backoff @@ -163,7 +163,7 @@ let client = BasecampClient( ## Services -The SDK exposes `55` account-scoped services. The tables below group the common ones; see `Sources/Basecamp/Generated/Services/` for the full set. +The SDK exposes `54` account-scoped services. The tables below group the common ones; see `Sources/Basecamp/Generated/Services/` for the full set. ### Projects & Organization diff --git a/swift/Sources/Basecamp/Generated/AccountClient+Services.swift b/swift/Sources/Basecamp/Generated/AccountClient+Services.swift index ee7f3d8dc..112e9dfec 100644 --- a/swift/Sources/Basecamp/Generated/AccountClient+Services.swift +++ b/swift/Sources/Basecamp/Generated/AccountClient+Services.swift @@ -4,7 +4,6 @@ import Foundation extension AccountClient { public var account: AccountService { service("account") { AccountService(accountClient: self) } } public var attachments: AttachmentsService { service("attachments") { AttachmentsService(accountClient: self) } } - public var automation: AutomationService { service("automation") { AutomationService(accountClient: self) } } public var bookmarks: BookmarksService { service("bookmarks") { BookmarksService(accountClient: self) } } public var boosts: BoostsService { service("boosts") { BoostsService(accountClient: self) } } public var bubbleUps: BubbleUpsService { service("bubbleUps") { BubbleUpsService(accountClient: self) } } diff --git a/swift/Sources/Basecamp/Generated/Services/AutomationService.swift b/swift/Sources/Basecamp/Generated/Services/AutomationService.swift deleted file mode 100644 index a6d7acc0e..000000000 --- a/swift/Sources/Basecamp/Generated/Services/AutomationService.swift +++ /dev/null @@ -1,13 +0,0 @@ -// @generated from OpenAPI spec — do not edit directly -import Foundation - -public final class AutomationService: BaseService, @unchecked Sendable { - public func listLineupMarkers() async throws -> [LineupMarker] { - return try await request( - OperationInfo(service: "Automation", operation: "ListLineupMarkers", resourceType: "lineup_marker", isMutation: false), - method: "GET", - path: "/lineup/markers.json", - retryConfig: Metadata.retryConfig(for: "ListLineupMarkers") - ) - } -} diff --git a/swift/Sources/Basecamp/Generated/Services/LineupService.swift b/swift/Sources/Basecamp/Generated/Services/LineupService.swift index 97bfd1b0f..eb3fb1bd5 100644 --- a/swift/Sources/Basecamp/Generated/Services/LineupService.swift +++ b/swift/Sources/Basecamp/Generated/Services/LineupService.swift @@ -21,6 +21,15 @@ public final class LineupService: BaseService, @unchecked Sendable { ) } + public func listLineupMarkers() async throws -> [LineupMarker] { + return try await request( + OperationInfo(service: "Lineup", operation: "ListLineupMarkers", resourceType: "lineup_marker", isMutation: false), + method: "GET", + path: "/lineup/markers.json", + retryConfig: Metadata.retryConfig(for: "ListLineupMarkers") + ) + } + public func update(markerId: Int, req: UpdateLineupMarkerRequest) async throws { try await requestVoid( OperationInfo(service: "Lineup", operation: "UpdateLineupMarker", resourceType: "lineup_marker", isMutation: true, resourceId: markerId), diff --git a/swift/Sources/BasecampGenerator/ServiceGrouper.swift b/swift/Sources/BasecampGenerator/ServiceGrouper.swift index b375ed0ed..b9b3de52b 100644 --- a/swift/Sources/BasecampGenerator/ServiceGrouper.swift +++ b/swift/Sources/BasecampGenerator/ServiceGrouper.swift @@ -56,9 +56,6 @@ let serviceSplits: [String: [String: [String]]] = [ "Dock": [ "Tools": ["GetTool", "UpdateTool", "DeleteTool", "CreateTool", "EnableTool", "DisableTool", "RepositionTool"], ], - "Lineup": [ - "Automation": ["ListLineupMarkers"], - ], "Recordings": [ "Events": ["ListEvents"], ], diff --git a/typescript/scripts/generate-services.ts b/typescript/scripts/generate-services.ts index 74915d635..064a8f737 100644 --- a/typescript/scripts/generate-services.ts +++ b/typescript/scripts/generate-services.ts @@ -217,9 +217,6 @@ const SERVICE_SPLITS: Record> = { Dock: { Tools: ["GetTool", "UpdateTool", "DeleteTool", "CreateTool", "EnableTool", "DisableTool", "RepositionTool"], }, - Lineup: { - Automation: ["ListLineupMarkers"], - }, Recordings: { Events: ["ListEvents"], }, diff --git a/typescript/src/client.ts b/typescript/src/client.ts index 02f81c341..49b2c4ef0 100644 --- a/typescript/src/client.ts +++ b/typescript/src/client.ts @@ -71,7 +71,6 @@ import { SearchService } from "./generated/services/search.js"; import { ReportsService } from "./generated/services/reports.js"; import { TemplatesService } from "./generated/services/templates.js"; import { LineupService } from "./generated/services/lineup.js"; -import { AutomationService } from "./generated/services/automation.js"; import { TodolistGroupsService } from "./generated/services/todolist-groups.js"; import { ToolsService } from "./generated/services/tools.js"; import { TimesheetsService } from "./generated/services/timesheets.js"; @@ -212,8 +211,6 @@ export interface BasecampClient extends RawClient { readonly templates: TemplatesService; /** Lineup service - manage timeline markers */ readonly lineup: LineupService; - /** Automation service - lineup marker listings and other automation */ - readonly automation: AutomationService; /** Todolist groups service - manage groups within todolists */ readonly todolistGroups: TodolistGroupsService; /** Tools service - manage project dock tools */ @@ -525,7 +522,6 @@ export function createBasecampClient(options: BasecampClientOptions): BasecampCl defineService("reports", () => new ReportsService(client, hooks, fetchPage, maxPages)); defineService("templates", () => new TemplatesService(client, hooks, fetchPage, maxPages)); defineService("lineup", () => new LineupService(client, hooks, fetchPage, maxPages)); - defineService("automation", () => new AutomationService(client, hooks, fetchPage, maxPages)); defineService("todolistGroups", () => new TodolistGroupsService(client, hooks, fetchPage, maxPages)); defineService("tools", () => new ToolsService(client, hooks, fetchPage, maxPages)); defineService("timesheets", () => new TimesheetsService(client, hooks, fetchPage, maxPages)); diff --git a/typescript/src/generated/services/automation.ts b/typescript/src/generated/services/automation.ts deleted file mode 100644 index 6f6fa2d01..000000000 --- a/typescript/src/generated/services/automation.ts +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Automation service for the Basecamp API. - * - * @generated from OpenAPI spec - do not edit directly - */ - -import { BaseService } from "../../services/base.js"; -import type { components } from "../schema.js"; - -// ============================================================================= -// Types -// ============================================================================= - -/** LineupMarker entity from the Basecamp API. */ -export type LineupMarker = components["schemas"]["LineupMarker"]; - -// ============================================================================= -// Service -// ============================================================================= - -/** - * Service for Automation operations. - */ -export class AutomationService extends BaseService { - - /** - * List all lineup markers for the account - * @returns Array of LineupMarker - * - * @example - * ```ts - * const result = await client.automation.listLineupMarkers(); - * ``` - */ - async listLineupMarkers(): Promise { - const response = await this.request( - { - service: "Automation", - operation: "ListLineupMarkers", - resourceType: "lineup_marker", - isMutation: false, - }, - () => - this.client.GET("/lineup/markers.json", { - }) - ); - return response ?? []; - } -} \ No newline at end of file diff --git a/typescript/src/generated/services/index.ts b/typescript/src/generated/services/index.ts index ee5c83c96..d775dbcd7 100644 --- a/typescript/src/generated/services/index.ts +++ b/typescript/src/generated/services/index.ts @@ -24,7 +24,6 @@ export { DocumentsService } from "./documents.js"; export { EventFeedService } from "./event-feed.js"; export { GaugesService } from "./gauges.js"; export { ForwardsService } from "./forwards.js"; -export { AutomationService } from "./automation.js"; export { LineupService } from "./lineup.js"; export { MessageBoardsService } from "./message-boards.js"; export { MessagesService } from "./messages.js"; diff --git a/typescript/src/generated/services/lineup.ts b/typescript/src/generated/services/lineup.ts index a1680fa6f..b55f70e79 100644 --- a/typescript/src/generated/services/lineup.ts +++ b/typescript/src/generated/services/lineup.ts @@ -12,6 +12,8 @@ import { Errors } from "../../errors.js"; // Types // ============================================================================= +/** LineupMarker entity from the Basecamp API. */ +export type LineupMarker = components["schemas"]["LineupMarker"]; /** * Request parameters for create. @@ -43,6 +45,30 @@ export interface UpdateLineupRequest { */ export class LineupService extends BaseService { + /** + * List all lineup markers for the account + * @returns Array of LineupMarker + * + * @example + * ```ts + * const result = await client.lineup.listLineupMarkers(); + * ``` + */ + async listLineupMarkers(): Promise { + const response = await this.request( + { + service: "Lineup", + operation: "ListLineupMarkers", + resourceType: "lineup_marker", + isMutation: false, + }, + () => + this.client.GET("/lineup/markers.json", { + }) + ); + return response ?? []; + } + /** * Create a new lineup marker * @param req - Lineup_marker creation parameters diff --git a/typescript/src/index.ts b/typescript/src/index.ts index 8657cee9f..898d59ed5 100644 --- a/typescript/src/index.ts +++ b/typescript/src/index.ts @@ -291,7 +291,7 @@ export { ClientVisibilityService, } from "./generated/services/client-visibility.js"; -// Automation services - generated +// Webhooks service - generated export { WebhooksService, type Webhook, @@ -523,16 +523,11 @@ export { // Lineup service - generated export { LineupService, + type LineupMarker, type CreateLineupRequest, type UpdateLineupRequest, } from "./generated/services/lineup.js"; -// Automation service - generated -export { - AutomationService, - type LineupMarker, -} from "./generated/services/automation.js"; - // Organization services - generated // // No `TodolistGroup` type: #544 consolidated it and `TodolistOrGroup` into the diff --git a/typescript/tests/services/automation.test.ts b/typescript/tests/services/automation.test.ts deleted file mode 100644 index 17de2f299..000000000 --- a/typescript/tests/services/automation.test.ts +++ /dev/null @@ -1,64 +0,0 @@ -/** - * Tests for the AutomationService (generated from OpenAPI spec) - */ -import { describe, it, expect, beforeEach } from "vitest"; -import { http, HttpResponse } from "msw"; -import { server } from "../setup.js"; -import { createBasecampClient } from "../../src/client.js"; -import type { BasecampClient } from "../../src/client.js"; - -const BASE_URL = "https://3.basecampapi.com/12345"; - -describe("AutomationService", () => { - let client: BasecampClient; - - beforeEach(() => { - client = createBasecampClient({ - accountId: "12345", - accessToken: "test-token", - enableRetry: false, - }); - }); - - describe("listLineupMarkers", () => { - it("should return lineup markers", async () => { - server.use( - http.get(`${BASE_URL}/lineup/markers.json`, () => { - return HttpResponse.json([ - { - id: 1069479400, - name: "Product Launch", - date: "2024-03-01", - created_at: "2024-02-15T10:30:00.000Z", - updated_at: "2024-02-15T10:30:00.000Z", - }, - { - id: 1069479401, - name: "Quarterly Review", - date: "2024-06-15", - created_at: "2024-03-01T09:00:00.000Z", - updated_at: "2024-03-01T09:00:00.000Z", - }, - ]); - }) - ); - - const markers = await client.automation.listLineupMarkers(); - expect(markers).toHaveLength(2); - expect(markers[0]!.id).toBe(1069479400); - expect(markers[0]!.name).toBe("Product Launch"); - expect(markers[0]!.date).toBe("2024-03-01"); - }); - - it("should return empty array when no markers", async () => { - server.use( - http.get(`${BASE_URL}/lineup/markers.json`, () => { - return HttpResponse.json([]); - }) - ); - - const markers = await client.automation.listLineupMarkers(); - expect(markers).toHaveLength(0); - }); - }); -}); diff --git a/typescript/tests/services/lineup.test.ts b/typescript/tests/services/lineup.test.ts index a112ef262..3c05175cd 100644 --- a/typescript/tests/services/lineup.test.ts +++ b/typescript/tests/services/lineup.test.ts @@ -111,4 +111,46 @@ describe("LineupService", () => { await expect(client.lineup.delete(999)).rejects.toThrow(BasecampError); }); }); + + describe("listLineupMarkers", () => { + it("should return lineup markers", async () => { + server.use( + http.get(`${BASE_URL}/lineup/markers.json`, () => { + return HttpResponse.json([ + { + id: 1069479400, + name: "Product Launch", + date: "2024-03-01", + created_at: "2024-02-15T10:30:00.000Z", + updated_at: "2024-02-15T10:30:00.000Z", + }, + { + id: 1069479401, + name: "Quarterly Review", + date: "2024-06-15", + created_at: "2024-03-01T09:00:00.000Z", + updated_at: "2024-03-01T09:00:00.000Z", + }, + ]); + }) + ); + + const markers = await client.lineup.listLineupMarkers(); + expect(markers).toHaveLength(2); + expect(markers[0]!.id).toBe(1069479400); + expect(markers[0]!.name).toBe("Product Launch"); + expect(markers[0]!.date).toBe("2024-03-01"); + }); + + it("should return empty array when no markers", async () => { + server.use( + http.get(`${BASE_URL}/lineup/markers.json`, () => { + return HttpResponse.json([]); + }) + ); + + const markers = await client.lineup.listLineupMarkers(); + expect(markers).toHaveLength(0); + }); + }); }); From f888185a6ac768e9a9bb69e10c1ac96badfd4f3e Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Fri, 18 Sep 2026 13:18:01 -0700 Subject: [PATCH 4/6] Carry the service drop through Appendix F and two stale asides MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Appendix F states each SDK's account-scoped surface as plain prose, so only the Swift and Kotlin rows are marked spans doc-constants rewrites. Ruby, TypeScript, Python and Rust still read 55; they lose AutomationService here like everyone else and are 54. sync-doc-constants' rationale still said Python was two short. #732 wired gauges and my_notifications, Appendix F's own Python row says so, and the parity gate counts Python at the full set. Dropped rather than left contradicting the row three lines away. The tags.smithy note said the method becomes LineupService#list. It does not — list_lineup_markers keeps its name and changes only the service it hangs off, which is what the generated services and MIGRATING.md both say. --- SPEC.md | 8 ++++---- scripts/sync-doc-constants.rb | 6 +++--- spec/overlays/tags.smithy | 5 +++-- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/SPEC.md b/SPEC.md index 45cb4ecbe..717829a84 100644 --- a/SPEC.md +++ b/SPEC.md @@ -5510,11 +5510,11 @@ SDK's own hand-written file. |-----|------------------------| | Swift | `54` — full canonical set (`AccountClient+Services.swift`, generated; one of §5's two sources) | | Kotlin | `54` — full canonical set (`ServiceAccessors.kt`, generated; §5's other source). Eight accessors expose handwritten composites that subclass their generated service — `cards`, `comments`, `documents`, `recordings`, `schedules`, `todolists`, `todos`, `uploads`, per the generator's `HAND_WRITTEN_SERVICES` — and the rest are the generated classes directly. The accessor set is identical either way, which is why §5 derives from this file regardless | -| Ruby | 55 — full canonical set. Held by its own accessor-roster test (`ruby/test/basecamp/accessor_inventory_test.rb`, added in #755) deriving the roster from `lib/basecamp/generated/services/`, so the next unwired service fails rather than going unnoticed. The seven hand-written composites are `prepend`ed onto their generated classes rather than subclassing them, so every accessor's class is the generated constant exactly — the five merge-safe writes, plus `recordings` and `comments` for the recording-summary and mention helpers (Appendix F), which is the count `ruby/lib/basecamp.rb`'s `on_load` hooks hold. | -| TypeScript | 55 — full canonical set, on the flat client alongside `authorization` (no `AccountClient` tier; see Client Topology above). Held by its own accessor-roster tests (`typescript/tests/accessor-inventory.test.ts` and `tests/types/accessor-inventory.test-d.ts`, added in #755) deriving the roster from `src/generated/services/`. Four hand-maintained renderings, so two instruments: the imports and `defineService` calls are resolved on a constructed client, the `index.ts` export blocks get their own assertion (a missing export is invisible at runtime to an in-repo importer and only bites a consumer), and the `BasecampClient` interface is asserted type-level, the factory returning `client as BasecampClient` so no runtime check can see it. Eight accessors expose hand-written composites that subclass their generated service, which the class assertions allow for: `todos`, `todolists`, `cards`, `documents`, `uploads`, `schedules`, and — since the recording-summary port — `comments` and `recordings`. Named rather than counted, because a count is stale the moment a composite is added and nobody re-reads an inventory row to notice. | +| Ruby | 54 — full canonical set. Held by its own accessor-roster test (`ruby/test/basecamp/accessor_inventory_test.rb`, added in #755) deriving the roster from `lib/basecamp/generated/services/`, so the next unwired service fails rather than going unnoticed. The seven hand-written composites are `prepend`ed onto their generated classes rather than subclassing them, so every accessor's class is the generated constant exactly — the five merge-safe writes, plus `recordings` and `comments` for the recording-summary and mention helpers (Appendix F), which is the count `ruby/lib/basecamp.rb`'s `on_load` hooks hold. | +| TypeScript | 54 — full canonical set, on the flat client alongside `authorization` (no `AccountClient` tier; see Client Topology above). Held by its own accessor-roster tests (`typescript/tests/accessor-inventory.test.ts` and `tests/types/accessor-inventory.test-d.ts`, added in #755) deriving the roster from `src/generated/services/`. Four hand-maintained renderings, so two instruments: the imports and `defineService` calls are resolved on a constructed client, the `index.ts` export blocks get their own assertion (a missing export is invisible at runtime to an in-repo importer and only bites a consumer), and the `BasecampClient` interface is asserted type-level, the factory returning `client as BasecampClient` so no runtime check can see it. Eight accessors expose hand-written composites that subclass their generated service, which the class assertions allow for: `todos`, `todolists`, `cards`, `documents`, `uploads`, `schedules`, and — since the recording-summary port — `comments` and `recordings`. Named rather than counted, because a count is stale the moment a composite is added and nobody re-reads an inventory row to notice. | | Go | 53 accessors. One service is folded rather than missing: `clientVisibility`'s sole operation is `RecordingsService.SetClientVisibility`. `timesheets` is spelled `Timesheet` (singular). Capability is 54/54; the surface is not. Hand-written service wrappers around the generated OpenAPI client — not fully generated. | -| Python | 55 — full canonical set. `gauges` and `my_notifications` were a wiring gap rather than a fold, and were wired in #732; the same change added an accessor-inventory test (`python/tests/test_client.py`) deriving its roster from `generated/services/`, so the next unwired service fails rather than going unnoticed. Sync and async agree exactly. | -| Rust | 55 — full canonical set; `accessors.rs`, generated, read by `make check-service-inventory-parity` | +| Python | 54 — full canonical set. `gauges` and `my_notifications` were a wiring gap rather than a fold, and were wired in #732; the same change added an accessor-inventory test (`python/tests/test_client.py`) deriving its roster from `generated/services/`, so the next unwired service fails rather than going unnoticed. Sync and async agree exactly. | +| Rust | 54 — full canonical set; `accessors.rs`, generated, read by `make check-service-inventory-parity` | No row rests on a dated hand-verification any more. The 2026-08-13 sweep of the accessor declarations (Go `AccountClient` methods, Ruby `Client#for_account` diff --git a/scripts/sync-doc-constants.rb b/scripts/sync-doc-constants.rb index 672d3f656..7f163ba43 100755 --- a/scripts/sync-doc-constants.rb +++ b/scripts/sync-doc-constants.rb @@ -366,9 +366,9 @@ def tracked_fixtures # rather than leaving it to be inferred: their entire reason to exist is hanging # services off AccountClient, which is exactly what §5's roster claims. Per-SDK # client wiring cannot answer the question — it drifts by design (Go folds -# `clientVisibility` and spells `timesheets` singular; Python is -# two short; TypeScript has no account-scoped tier at all), so a roster derived -# from any one of them would assert that SDK's gaps as the canonical surface. +# `clientVisibility` and spells `timesheets` singular; TypeScript has no +# account-scoped tier at all), so a roster derived from any one of them would +# assert that SDK's gaps as the canonical surface. KOTLIN_ACCESSORS = "kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/generated/ServiceAccessors.kt" SWIFT_ACCESSORS = "swift/Sources/Basecamp/Generated/AccountClient+Services.swift" diff --git a/spec/overlays/tags.smithy b/spec/overlays/tags.smithy index 07c4e7c75..a7a9293e1 100644 --- a/spec/overlays/tags.smithy +++ b/spec/overlays/tags.smithy @@ -231,8 +231,9 @@ apply RepositionTool @tags(["Dock"]) // without a SERVICE_SPLITS entry, so it fell through to the tag-derived service // and generated into AutomationService alone while its three siblings were // split into Lineup. With no entry carrying it, retagging lands it on Lineup -// with them, AutomationService stops being emitted, and every SDK's -// AutomationService#list_lineup_markers becomes LineupService#list. That is a +// with them and AutomationService stops being emitted: the method keeps its +// name and changes the service it hangs off, so every SDK's +// automation.list_lineup_markers becomes lineup.list_lineup_markers. That is a // breaking change, taken deliberately and recorded in MIGRATING.md. apply ListLineupMarkers @tags(["Lineup"]) apply CreateLineupMarker @tags(["Lineup"]) From 2fc8aa05a4c9d08421016be9332311ed1e14caee Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Fri, 18 Sep 2026 13:19:17 -0700 Subject: [PATCH 5/6] Document the two public fields that carry the tag Match.Resource and catalog.Operation.Tag both restate an operation's tag verbatim, so both change value for all 37 retagged operations with no compile error anywhere. A Go caller switching on Resource == "Automation" simply stops matching. MIGRATING.md only covered the ListLineupMarkers service move, which is the loud half. Also drop Lineup from README's Automation coverage row. It is listed under Scheduling already, and adding it to a row this change empties implies an Automation grouping still exposes it. --- MIGRATING.md | 22 ++++++++++++++++++++++ README.md | 2 +- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/MIGRATING.md b/MIGRATING.md index af90268aa..d0210a9f0 100644 --- a/MIGRATING.md +++ b/MIGRATING.md @@ -44,6 +44,28 @@ splitting it into real domains is what the rest of that change does. `Automation existed only because this one operation fell through the tag's split table while its three siblings were routed out of it; the tag it was named after no longer exists. +### The `Automation` tag is gone, and two public fields carry it + +Splitting that catch-all retags 37 operations into `Templates`, `Webhooks`, `Dock`, +`Lineup`, `Search` and `Checkins`. Two public surfaces restate an operation's tag +verbatim, so both change value for those 37 — **silently, with no compile error.** + +*Go's URL router.* `Router.Match(url).Resource` is the route table's resource field, +which is the tag. A URL under `/templates/`, `/webhooks/`, `/dock/tools/`, +`/recordings/{toolId}/position`, `/lineup/markers`, `/search`, `/searches/metadata`, +`/questionnaires/`, `/questions/` or `/question_answers/` used to classify as +`"Automation"` and now classifies as its own domain. A `switch m.Resource` or +`if m.Resource == "Automation"` compiles and stops matching. Match the new label, or +switch on `m.Operation`, which did not change. + +*The embedded catalog.* `catalog.Operation.Tag` and the keys of `Catalog.ByTag()` +move the same way for the same 37 operations. A consumer that groups by tag — which +is what the catalog is for — gets six groups where it got one, which is the point of +the change; a consumer that hard-codes `"Automation"` gets nothing. + +Nothing else reads the tag. Service names, method names, signatures, URLs and +operation ids are unchanged for all 37; only `ListLineupMarkers` moves service, above. + ### Go: the event feed's poll lanes refuse a malformed response instead of typing it `EventFeedService.PollEvents` and `PollInbox` now decide each response against the diff --git a/README.md b/README.md index c9259f78f..5fe6bdfb8 100644 --- a/README.md +++ b/README.md @@ -250,7 +250,7 @@ All SDKs provide: | **Files** | Vaults, Documents, Uploads, Attachments | | **Card Tables** | CardTables, Cards, CardColumns, CardSteps, Wormholes | | **Client Portal** | ClientApprovals, ClientCorrespondences, ClientReplies | -| **Automation** | Webhooks, Subscriptions, Events, Lineup | +| **Automation** | Webhooks, Subscriptions, Events | | **Reporting** | Search, Reports, Timesheets, Recordings | ## Specification From d339dbd142af6ba349ac819e3b5ef91f8b97ab80 Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Fri, 18 Sep 2026 13:23:18 -0700 Subject: [PATCH 6/6] Point the v0.19.0 roster at where that accessor went Its list of unpaginated Python operations names automation.list_lineup_markers, which no longer exists. Renaming it in place would make the v0.19.0 section describe an API v0.19.0 did not have, and that section is read by someone upgrading TO v0.19.0. Naming both instead: what it was there, and where it is now. --- MIGRATING.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/MIGRATING.md b/MIGRATING.md index d0210a9f0..aa4e1d1c6 100644 --- a/MIGRATING.md +++ b/MIGRATING.md @@ -316,7 +316,9 @@ capability: `['id']` and `['a', 'b', 'c']` were wrong answers presented as successes. If you were consuming them, the new refusal is telling you something your code was previously acting on. -The 9 unpaginated operations, sync and async: `automation.list_lineup_markers`, +The 9 unpaginated operations, sync and async: `automation.list_lineup_markers` +(the accessor as it stood at v0.19.0; it is `lineup.list_lineup_markers` from the +Unreleased section above onward), `everything.get_everything_overdue_cards`, `everything.get_everything_overdue_todos`, `folders.list_folders`, `my_assignments.get_my_completed_assignments`,