Up Next priority writes + provenance repin to the exact-target contract (#12380, #12483) - #528
Merged
Conversation
BC3 #12380 documented the three write operations behind My Assignments'
Up Next list; the SDK modeled only the retrieval side. All three land
on the existing MyAssignments tag: PrioritizeAssignment (POST
/my/priorities.json {id}, 204, idempotent — Assignment#prioritize is
priority || create_priority!, a repeat is a pure no-op),
DeprioritizeAssignment (DELETE /my/priorities/{recordingId}, 204,
idempotent only because BC3 #12483 made the JSON path exact-target with
delete-of-absent as a no-op), and ReorderUpNext (POST
/my/priority_moves.json {source_id, position}, 204, deliberately NOT
retry-gated: a positional move's meaning shifts as the list changes,
with the typed 400 / flat-{error} 422 / bare 404 contract from #12380's
hardening). 235 -> 238 operations.
The pin moves e83b2733 -> d0edc1283b (bc3 #12483's merge). Full drift triage of the range: #12478 test-DB seeding, #12479 scrape-time gauges, #12480 boost-feed render preload, #12481 backpressure config removal, a completion-lock race fix, relay-revocation ops tooling, #12444 bcenv retirement — all wire-neutral internals; the only doc/api or routes change in the entire range is my_assignments.md's exact-target Deprioritize contract, absorbed by this PR. provenance-sync propagated the API version date (2026-07-31) into every SDK's version constants.
Go's MyAssignmentsService gains the three write methods (Reorder validates the 1-based position client-side and is never retried). Python's sync AND async clients gain the my_assignments accessor — it was never wired when the retrieval side shipped, so the generated service was unreachable; the classic sync-only-wiring miss, caught by this PR's tests.
TS/Ruby/Python pin the exact wire bodies ({id}, {source_id, position}),
the exact-target DELETE path, the typed 400/422 flat-{error} bodies,
and the bare bodyless 404. Conformance: three paths cases, idempotent-
retry cases for Prioritize (POST 503->204) and Deprioritize (DELETE
503->204) with Ruby's GET-only skips, and a ReorderUpNext no-retry case
(503 -> exactly one request) pinning the deliberate non-idempotent
classification. Dispatch in all five runners.
The sprint's BC3-fixed end state (238/122/77/199), matching behavior-model exactly. Parity 75->77 / 197->199; SPEC ceiling narrative + Appendix E; SECURITY 23 DELETEs / 7 flagged POSTs / 39 other POSTs. The gap file's original claim that the DELETE was idempotent as-shipped is corrected on the record — it was wrong until bc3 #12483 — and the entry flips to absorbed-in-sdk with three smithy_refs, closing the sprint at 8 of 9 (schedule-recurrence-writes stays honestly gated on its live probe).
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e1eff23971
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Codex caught the mismatch (the C8 FieldValidationError catch's sibling):
NotFoundError requires an {error} body, but the moves controller's
inaccessible-recording 404 is a bare head :not_found with no payload.
New zero-member BareNotFoundError error shape — the generated OpenAPI
404 response now carries no content schema, so strict consumers won't
try to decode a body the server never sends. Runtime error-class
mapping is status-driven and unchanged (the per-SDK bare-404 tests
already pin NotFound classes against an empty body).
This was referenced Jul 31, 2026
Merged
Merged
jeremy
added a commit
that referenced
this pull request
Aug 3, 2026
…ge (#593) * Repin bc3 to 2c0dafba13 and absorb the Folders API it brings into range The repin and the absorption are one change on purpose. `doc/api/sections/ folders.md` does not exist at the old pin `d0edc1283b`, so the five `/stacks` routes are absent from `spec/bc3-routes.json` today. Repinning alone would make them appear in the route table with no operation modelling them, and the #586 parity gate would then demand a disposition for five routes we have every reason to model. Absorbing alone would model a contract the pin says isn't there. 238 -> 243 operations. ## The repin: d0edc1283b -> 2c0dafba13 (11 commits) Exactly one touches API surface: - `dc6cd10714` #12384, the Folders API — the absorption half, below. Two more are behaviour fixes to endpoints already modelled, with the wire shape unchanged, so they need the repin and nothing else: `344581a379` (#12494) and `2c0dafba13` (#12501) stop a draft's subscribers being recomputed when an update doesn't address them. They add prose to five `doc/api` sections but no route and no payload field — proven by regenerating the route table, whose only delta is the five `/stacks` routes. `19956c5579` (#12488) deserves its own sentence because it *deletes* a `request.format.json?` branch, which reads like an API change. It is not. The removed branch returned `nil` for JSON when the recording's own assignment wasn't prioritized; the replacement returns that same assignment unconditionally and `&.unprioritize` is a no-op when there is no priority. Both spellings are exact-target on the id in the URL and both answer `204`. What actually changed is that the *web* path stopped cascading to a sibling step — it adopted the contract the JSON API has had since bc3#12483 (absorbed in SDK #528). The remaining seven are four dev-tooling (`112c1664bf`, `63a34abe30`, `5f889d8365`, `faf5fe9dfc`), two Turbo-morph web-only (`1fad1ef608`, `c664cdc9de`), and one push-notification backend swap (`845df62b6b`). `spec/bc3-routes.json` is regenerated at the new pin (369 routes, 64 sections); `make provenance-sync` keeps the go:embed copy in step. The API version tracks the pin date, so every SDK's `API_VERSION` moves to 2026-08-02. ## The absorption: FoldersService, five operations Folders group projects on one person's home screen. They are per-user, so the collection is flat rather than bucket-scoped. **The wire type stays `Stack`** — the product was renamed, the payload was not — and so does the URI segment. The operations, structures and generated methods use `Folder`. ListFolders GET /{accountId}/stacks.json 200, bare array GetFolder GET /{accountId}/stacks/{folderId} 200 CreateFolder POST /{accountId}/stacks.json 201 UpdateFolder PUT /{accountId}/stacks/{folderId} 200 DeleteFolder DELETE /{accountId}/stacks/{folderId} 204 Two output structures, not one. `Folder` is what the list returns; only `FolderWithProjects` carries the expanded `projects`. A single shape with an optional member would make every generated list-item type declare a field the list response never populates. `gauges_url`, `color` and `image_url` are **required-and-nullable** (SPEC §"A third wire state"): the jbuilder emits all three unconditionally and all three are commonly `null`, so `@required` models the presence and `smithy-build.json` `jsonAdd` layers `type: ["string","null"]` on top. Go types them `*string` because the value is nullable, not because the key is optional. Same treatment `Wormhole.color`/`destination_url` already get. `project_ids` does not round-trip: the create input takes it, and the response reports the same ids as `bucket_ids` *and* expands them as `projects`. Three names, one relationship. An unreachable id fails the whole request with 404 and writes nothing, so `CreateFolder` lists `NotFoundError`; `UpdateFolder` lists `ValidationError` because a blank name is a real 422 (create defaults it to "New folder" instead). `UpdateFolder` and `DeleteFolder` are naturally idempotent; `CreateFolder` is not, and gets the inert retry block every non-idempotent POST carries. The `Folders` tag resolves to `FoldersService` through every generator's default fallback — zero service-group overrides, and `ListFolders` infers the `folder` resource type cleanly, so no override there either. One deliberate generator addition: `GetFolder` -> `getFolder` in TS/Kotlin/Swift, because the bare rule collapses it to `folder(id)`, a noun with no verb, while Ruby and Python already emit `get_folder`. Same shape as the `GetBookmark` override; all six SDKs now agree on listFolders/getFolder/createFolder/updateFolder/deleteFolder. Go's `FolderWithProjects` repeats the base fields rather than embedding `Folder`. Embedding promotes them correctly at runtime but hides them from `check-wrapper-drift`, which walks declared fields and reported all twelve as missing. Repeating them keeps the guard able to verify every field. ## Registry and coordination `spec/api-gaps/folders-api.md` moves to `absorbed-in-sdk` with seven `smithy_refs`. `spec/api-gaps/stack-doc-and-smithy.md` deliberately keeps its `confirmed-not-api-resource` status: it records a decision that was correct when made, and the supersession note in `folders-api.md` exists precisely to preserve that history. `COORDINATION.md` named pin `338b7a11`, three repins stale; it now names `2c0dafba13` and says the provenance file is the only authority. ## Verified `make check` exit 0. Swift is not skipped on this host: `swift build`/`swift test` ran 306 tests with 0 failures, and `swift run ConformanceRunner` ran the conformance suite. Ruby 1092 runs / 0 failures. Conformance 147 passed, 1 skipped (a pre-existing Go-runner origin case), and the five new `paths.json` cases PASS in **all six** runners — Go, Kotlin, TypeScript, Ruby, Python and Swift each have a dispatch branch, so none of them silently skips. `BC3_REPO_PATH=... make bc3-routes-check` exit 0. Counts updated deliberately: `check-idempotency-parity` 77/199 -> 79/203, SPEC/SECURITY/AGENTS 238 -> 243 operations, 122 -> 124 GETs, 47 -> 48 PUTs, 23 -> 24 DELETEs, 39 -> 40 single-attempt POSTs, 188 -> 192 retry-eligible ops at max 3. * Folder writes declare FieldValidationError — the 422 they actually render CreateFolder and UpdateFolder declared `ValidationError`, whose required wire member is a singular `error` string. bc3 renders neither create nor update that way. `stacks_controller.rb:51` answers a rejected rename with `render json: { errors: @stack.errors }`, and `:27` answers a rejected create with the same rendering from its `RecordInvalid` rescue. `ActiveModel::Errors` serializes field-keyed, so the wire body is: {"errors":{"name":["can't be blank"]}} `FieldValidationError` already exists for exactly this shape and is already carried by `UpdateCalendar`, `UpdateMyNote` and `UpdateMyPreferences`. Both folder writes now use it. No other operation is touched. The failure this fixes is silent, which is why it earns a test. While the operations declared `ValidationError`, Go's typed `JSON422` was a `*ValidationErrorResponseContent`; decoding the field-keyed body into it succeeds — `encoding/json` does not care that `error` is absent — and leaves `JSON422.Error` as `""`. A caller reading the typed field got an empty string and no error of any kind. `TestFoldersGenerated_JSON422_DecodesFieldKeyedBody` goes at the generated client directly and pins `JSON422.Errors` for both operations. It was shown to fail before the fix: against the previous commit's `client.gen.go` it does not compile, because `*ValidationErrorResponseContent` has no `Errors` field. The two wrapper-level tests beside it (`TestFoldersService_{Update,Create}_ FieldKeyed422`) are deliberately not the proof. They were run against the un-fixed generated client and **passed**, because `checkResponse` parses the raw body itself and never reads the typed field. A wrapper-only assertion here would have been vacuous. The hand-written TS/Python/Ruby tests already modelled the field-keyed body, so they needed no change — they were right about the server and the spec was the wrong half. make check: REAL_EXIT=0. Swift ran rather than skipped (306 tests, 0 failures; `swift run ConformanceRunner` executed). Ruby 1092 runs, 0 failures. Conformance 147 passed / 1 skipped (pre-existing). * SPEC §2: the per-op retry.max distribution counts the five Folders ops Copilot caught this on #593. All five Folders operations carry `max: 3` in behavior-model.json, so the `max:3` bucket is 200, not 195 — and 200 + 43 = 243 matches the new operation total. The neighbouring derived figures in the same section (192 retry-eligible at max 3, 203 retry-eligible total) were updated with the absorption, so leaving 195 made the section internally inconsistent. Re-derived from behavior-model.json rather than incremented by hand. This prose carries no CI gate. * Swift conformance: read project_ids through the throwing intArray helper Copilot flagged this as a suppressed low-confidence comment on #593, and it is right. The Folders dispatch hand-rolled project_ids parsing with compactMap, which silently drops any element that is not an integer or that overflows Int — the exact "one step quieter" fault Dispatch.swift's own helper docstring (lines 19-24) exists to warn about. A quietly truncated array would still have satisfied the requestBody assertion. intArray (lines 77-90) already returns [Int]? and throws badParameter on a malformed element, and every other array read in this file goes through it. Mine was the only exception.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
C4 — the sprint-closing absorption, sequenced last on purpose: it carries the mandatory provenance repin past bc3 #12483.
Why last
DeprioritizeAssignmentcould not be honestly classified idempotent as BC3 shipped it: the JSON DELETE inherited the web UI's card→step fallback, so a retried DELETE whose first attempt committed cascaded to a child step and deleted a second, different priority. The upstream fix (bc3 #12483, exact-target + no-op-on-absent, with the card+child-step repeat-DELETE regression) merged today; this PR absorbs the surface and repins to it. The bc-api mirror sync is up as bc-api#428.What
PrioritizeAssignment(POST{id}, 204, idempotent — verified against the model:Assignment#prioritizeispriority || create_priority!, a repeat is a pure no-op),DeprioritizeAssignment(DELETE, 204, idempotent only because of #12483 — say so in its doc comment),ReorderUpNext(POST{source_id, position}, 204, deliberately not retry-gated — a positional move's meaning shifts as the list changes — with #12380's hardened contract: typed 400s, flat-{error}422s, bare bodyless 404).e83b2733 → d0edc1283bwith full drift triage in the commit message — the only doc/api or routes change in the entire range is the contract this PR absorbs; everything else is wire-neutral internals.provenance-syncpropagates the 2026-07-31 API version date into every SDK's constants.my_assignmentsaccessor — the generated service was unreachable since the retrieval side shipped (the classic sync-only-wiring miss, surfaced by this PR's tests).absorbed-in-sdk.schedule-recurrence-writesstays honestlyaddressed-in-bc3-pr-12359behind its human-credentialed live probe.Verification
Full
makegreen (exit 0) +make kt-check-generated-driftclean; 22 conformance passes for the new ops + the two justified Ruby skips; TS 8/8, Ruby 7/7, Python 8/8 unit tests.Summary by cubic
Adds Up Next write APIs to My Assignments—prioritize, exact-target deprioritize, and reorder—repins to the fixed #12483 contract, and models ReorderUpNext’s 404 as bodyless to match the server. Bumps API version to 2026-07-31.
New Features
PrioritizeAssignment(POST/my/priorities.jsonwith{id}): returns 204, idempotent, retries on 429/503.DeprioritizeAssignment(DELETE/my/priorities/{recordingId}): exact-target per #12483, returns 204, idempotent, retries on 429/503.ReorderUpNext(POST/my/priority_moves.jsonwith{source_id, position}): returns 204, not idempotent (no retries); typed 400, flat{error}422, and 404 modeled without a body to reflect the wire. Go validates 1-basedposition.my_assignmentsaccessor.behavior-model.jsonand generated files): 238 ops total; 77 idempotent mutations. Conformance/unit tests cover paths, retries, and the no-retry pin for reorder.Dependencies
2026-07-31in Go, Kotlin, Python, Ruby, Swift, and TypeScript.Written for commit 97eef8a. Summary will update on new commits.