--- name: distilled-sdk-update description: Move an existing distilled SDK to its mirror's latest spec, regenerate it, audit packages//patches/ with `pnpm patches:audit` and delete or slim the patches the new spec has absorbed, then open the PR. Use for "update to the latest spec", "regenerate ", "audit / remove unused patches", "which patches does the new spec no longer need", or when a provider says they fixed their spec. Adding, changing, merging or rebasing a single patch, including "do we still need this patch PR?", is distilled-sdk-patch, which reads the generated diff and runs no audit. When also asked to "look at / go through the open PRs" for that provider, it reconciles them against the new spec (Step 6). Building a new SDK is the distilled-sdk skill. --- # Updating a distilled SDK to its latest spec Every patch under `packages//patches/` is a claim that the upstream description is wrong. Upstreams fix things, so every regeneration is also the moment to drop the patches that no longer change anything. The two halves are one job: a spec update that keeps stale patches hides the fix, and a patch audit against an old spec finds nothing. Work in a worktree cut from `origin/main` and run `pnpm install` there. Nothing here needs a full `pnpm specs:sync`; one mirror is enough. ## Step 1 — move the mirror ```sh pnpm --filter @distilled.cloud/ run specs:fetch # initialise at the committed commit pnpm --filter @distilled.cloud/ run specs:update # move to the mirror's tip git -C packages//specs/spec-mirror- log -1 --format='%h %ci' ``` `specs:update` alone skips a submodule that was never initialised, which is why `specs:fetch` comes first. `git status` now shows the gitlink (`M packages//specs/spec-mirror-`) — that line is part of the commit; it is how anyone else reproduces the generation. The mirror refetches daily, so its tip is at most a day behind upstream. When you need today's upstream (a provider just published a fix), preview with `pnpm specs:local ` and `DISTILLED_SPECS_LOCAL=1 pnpm generate `, but commit only a generation from the mirror gitlink — a `.local` generation is one nobody can reproduce. ## Step 2 — regenerate ```sh pnpm generate ``` A patch whose pointer no longer resolves fails convert with `❌ bad patch: [ ]: stale target`. That is the new spec telling you something moved: either upstream now has what the op added (delete the op) or the path changed (re-point it). Fix the patch and rerun; `onStalePatch: "warn"` is how a whole chain once vanished silently. ## Step 3 — audit the patches ```sh pnpm patches:audit # one verdict per file pnpm patches:audit --ops # also one per op inside every needed file pnpm patches:audit --only pnpm patches:audit --jobs # parallel converts; default from cores and free memory ``` Run the audit only after the mirror has moved (step 1): it answers which patches the new spec has absorbed, and nothing else. Always name the package. To check what one patch you just wrote or edited does, read the generated diff instead (`distilled-sdk-patch`, step 5). The audit (`@distilled.cloud/core/codegen/patch-audit`) builds the model once with every patch, then once per patch with that patch left out, and diffs the result. It runs `convert` on scratch copies of the package (`packages/.audit--`, removed on exit; the package itself is never written), so the spec mirror must be fetched (step 1); without it the package is reported as skipped. Patches are Smithy ops applied in `finalizeConvert`, so each file is judged in memory: one convert, then only the finalize steps re-run per file. All of `cloudflare` (about 2,850 files) takes under two minutes, `posthog` and `azure` well under a minute, most packages seconds. Railway's GraphQL patches apply outside `finalizeConvert` and cost one convert each (`DISTILLED_SKIP_PATCHES`), spread over `--jobs` copies. | Verdict | Meaning | Do | | --- | --- | --- | | `🗑 no effect` | the model is byte-identical without it | delete the file | | `✔ needed` | the model differs; the pointers are listed | keep, and read the diff — it is the patch's description, mechanically | | `🔗 the build fails without it` | a later patch targets what this one adds | keep both, or delete both | | `op N: no effect` (`--ops`) | one op in a needed file is dead | remove that op | Verdicts are one-at-a-time. Two patches that add the same thing each look unused alone; only the second deletion changes the model. So: delete what it lists, `pnpm generate `, audit again, until the list is empty. Two things the audit cannot judge: - **Typed patch configs.** `packages/aws/patches/.json` is `applyAwsSpecPatches` config, not RFC-6902; the audit reports those files as not audited. Judge them by reading the model. - **Whether a needed patch is *right*.** A patch that still changes the model may still be wrong about the wire. Patches that encode observed behaviour — error statuses the spec omits, required fields the API actually omits, `x-sensitive` marks — are only confirmable against the live API. With credentials, probe: create throwaway resources, hit the mutating routes with invalid bodies, record the statuses, tear down promptly. Add only what you observed; a probe that never reached validation (401/404 on a dummy slug) proves nothing. Writing the patch that records it is the `distilled-sdk-patch` skill. While slimming, keep each file's `description` true to what is left, and fold files of the same kind together when they shrink to a few ops (two files that both only add omitted `422`s are one file). The description is the bug report you will send upstream. ## Step 4 — check the delta ```sh git diff --stat -- packages/ pnpm exec tsc -b packages/ --noCheck false # the root typecheck:ci is heavy pnpm lint ``` Read `git diff -- packages//src/services` for what upstream changed underneath you, beyond the patches: - **Renamed or removed operations.** An upstream `operationId` change renames an export; a removed path removes one. Both are breaking and belong in the PR body by name, old → new. - **Nullability and required flips** on shapes callers already use — `T` → `T | null` in an output is a type break too. - **Operation count** before and after, as one line. ## Step 5 — commit and PR Stage explicit paths — the gitlink, `.generated-specs/`, `patches/`, `src/services/`, and `README.md` if an example changed: ```sh git add -- packages//specs/spec-mirror- packages//.generated-specs \ packages//patches packages//src/services git commit -m "feat(): regenerate from the spec" ``` The PR body records what the next person needs to reproduce and review: - mirror commit before → after, and the date of the spec - operations before → after - patches deleted, one line each with *why* (already in the spec, never read by convert, overwrote what upstream now publishes) - patches kept, one line each with what the spec still gets wrong - renamed / removed operations That "patches kept" list, with each file's description and a link to it on the branch, is also the message to send the provider. Group it by kind — missing `x-nullable`, shared models with too many `required` fields, undocumented error statuses, a wrong response schema, secrets with no sensitive mark — because that is how they will fix it. ## Step 6 — reconcile open PRs for the provider (when asked) Open PRs that patch `packages/` were written against the old spec, so they are reviewed after the update has merged, never before. The new spec decides each one. The authors' commits should land with their names on them: update their branches and merge, and close only what the spec or another PR already covers. **Find them.** ```sh gh pr list --state open --limit 300 --json number,title,author,files \ --jq '.[] | select(any(.files[]; .path | startswith("packages//"))) | "#\(.number) \(.author.login): \(.title)"' ``` Filter on changed files: titles miss PRs scoped to `core` or to a sibling package that also patch this one. Read each body and its `patches/` diff. If the user excludes a PR or an API, leave it untouched: no push, no comment, no close. **Judge each against the regenerated model** (`.generated-specs/`, not the TypeScript): | Finding | Do | | --- | --- | | The spec now has it: same operations, members or shapes | close with a comment naming the spec commit and the generated symbols | | Another open PR does the same, or it already merged | merge the most complete one; close the rest with a link to it | | Part is in the spec now | trim the PR to what the spec still lacks | | None of it is in the spec | update and merge as is | | The spec has it, but the converter generates it wrong | fix the converter in its own PR, merge it, then close | Usually reading the regenerated model answers it: search `.generated-specs/` for the operations, members or shapes the PR adds. When it does not, merge `main` into the PR's branch and run `pnpm generate `: a stale target means the spec moved underneath the patch, and the generated diff against `main` shows what the patch still adds. Reach for `pnpm patches:audit --only --ops` only when that diff cannot tell you which ops are dead. **Update a branch in place.** Maintainers can push to forks when `maintainerCanModify` is true; for a fork, fetch `refs/pull//head`, since `origin/` does not exist: ```sh git fetch origin main "refs/pull//head:refs/remotes/pr/" git checkout -B pr- pr/ git merge --no-edit origin/main # merge, never rebase: keep their commits pnpm generate # resolve generated-file conflicts by regenerating pnpm exec tsc -b packages/ --noCheck false pnpm vitest run packages//src/.test.ts git push https://github.com//.git pr-: ``` - Conflicts in `.generated-specs/` or `src/services/` are not edits to resolve by hand: take either side and regenerate. Conflicts in `patches/` are real; read both sides. Two PRs adding the same shape merge into duplicate ops or duplicate JSON keys without a textual conflict, so check the patch parses with no repeated keys. - Delete a test the PR added that only checks the patch's generated shape; patches carry no per-package tests (`distilled-sdk-patch`, step 4). Other tests that predate repo changes (`bun:test` → `vitest`, `Redacted` credentials) get fixed in a separate commit on their branch. - When a PR was trimmed, say so in a comment on it: what was dropped and why. **Merge in dependency order.** PRs that regenerate the same service conflict with each other once one lands. Merge the independent ones first, then re-merge `main` into each remaining branch and regenerate before queueing it. `main` uses a merge queue; enqueue with the GraphQL `enqueuePullRequest` mutation (with `expectedHeadOid`) after the checks pass. **Ask before** closing anyone's PR, or before a change beyond patches, such as a converter or runtime fix. Every close carries a comment that thanks the author and names what superseded it, by PR number or spec commit.