Skip to content

Derive gen-catalog's emission bound from spec/generated-verbs.json - #938

Merged
jeremy merged 2 commits into
mainfrom
catalog-embed-derive-generated-verbs
Sep 20, 2026
Merged

jeremy merged 2 commits into
mainfrom
catalog-embed-derive-generated-verbs

Conversation

@jeremy

@jeremy jeremy commented Sep 19, 2026 •

Copy link
Copy Markdown
Member

What

Replace gen-catalog's hardcoded verb allow-list with a derive from spec/generated-verbs.json — the single shared declaration the six per-language SDK generators already read. This is the catalog-embed lane of basecamp-sdk#926.

gen-catalog was carrying its own copy of the emission bound (the set of HTTP methods the SDK generators emit). A copy of a fact that already has one authoritative source is exactly the divergence #925 was: six lists of one fact.

The derive (contract, locked with the tag-pipeline owner)

loadGeneratedVerbs — one small isolated function:

  • Reads spec/generated-verbs.json permissively with encoding/json (unknown keys ignored), takes the verbs strings, keeps only the non-empty, non-whitespace ones.
  • Fails loud (returns an error that stops generation) on both: (a) the file is unreadable/unparseable, and (b) it parsed but yields no usable verb (verbs absent, [], or nothing but blanks).
  • No fallback to a hardcoded default. A silent fallback is how a stale superset (e.g. one still naming patch) survives the file dropping the verb — the dangerous direction — so an empty/absent bound stops the build, it is never read as "no restriction."
  • No validator: no BOM/stream/single-document/shape checks — and none upstream either. One validator, six readers: stop the generators disagreeing about the shared declaration #933 would have added that gate and was closed unmerged, having shown a "prerequisite of every generate target" is unreachable (eight entry points, ending at make -j scheduling it concurrently with the checks it was meant to precede). A seventh JSON opinion cannot define validity for the other six. This function only asserts a fact about its own state: I obtained a usable set to bound on, or I stop.
  • No len(bound)==0 short-circuit downstream that could turn an empty bound into allow-everything. An empty map (never reachable via run, which errors first) would refuse every verb — the safe direction.

Kept in one function on purpose: basecamp-sdk#935 may replace the JSON read with a generated Go constant. That should be a one-function swap, not a rewrite (short comment in the code notes this).

Everything else is unchanged: the exclusion-based path-item slot discovery (isStructuralPathKey — $ref/summary/description/servers/parameters/x-), and the fail-loud-on-a-verb-outside-the-bound behavior — now the bound comes from the file, not a literal. The old five-verb map and the header comment that restated it are gone.

Byte-identical catalog

#932 removed patch from generated-verbs.json, so the derived bound differs from the old hardcoded set only by patch (unused — the API models no PATCH operation). No operation in the current spec uses a verb outside the generated set, so catalog.json regenerates byte-identical. Confirmed via make catalog-check. If a future rebase ever makes make catalog-check non-green, that is a finding to investigate (an operation reaching a verb the runtimes can't serve), not a rebase artifact to regenerate away.

Coupled doc edits carried in this same PR (one fact, one source)

scripts/gen-catalog/ lives inside basecamp-sdk, so the code derive and the prose that reasons about it are the same repo — one PR avoids a staleness window.

  1. spec/generated-verbs.json — the last sentence of the not_patch note described gen-catalog as owning its own bound and "only ever accept[ing] a superset." That goes false the moment this derive lands (the bound is now identical by construction, not a hand-maintained superset). Replaced with the owner-provided text naming the two consequences: adding a verb here widens gen-catalog automatically, and the criterion it inherits is the file's — whether all six runtimes can serve the verb.
  2. scripts/gen-catalog/main.go header — already stale on main (it said the generators "iterate exactly get/put/post/delete/patch", but A refusal must not destroy generated output: fixed in Kotlin and TypeScript, swept across the rest #932 removed patch). Rewritten to name the file as the source and drop the hardcoded list.

The bound's entry criterion (coupling worth knowing)

patch is absent from the array not because the API doesn't model it, but because 2 of 6 runtimes get it wrong: Ruby's BaseService has no http_patch (NoMethodError at call time) and Kotlin's ServiceEmitter renders PATCH via httpPut (wrong method on the wire). So the array's entry criterion is "all six runtimes can actually serve this verb," not "the spec models it." Consequence now that gen-catalog derives: adding a verb to generated-verbs.json automatically widens gen-catalog's accepted surface — correct for a catalog, but the next reader should know this bound now inherits a runtime-capability criterion, not a spec-modeling one.

Tests

New loadGeneratedVerbs tests: unreadable file → error; unparseable file → error; absent-verbs/[]/all-blank → error (fail-loud, no usable bound); a valid file → correct bound; blanks dropped while a usable verb remains → that set; and the committed generated-verbs.json loads to a non-empty bound that excludes patch (post-#932). Existing Build tests pass the bound explicitly via a testVerbs() helper; the #925 non-generated-verb guard still refuses head/options/trace.

Verification

  • make catalog-check → byte-identical (catalog.json up to date, 265 ops / 32 tags)
  • go build ./... (go module) → OK
  • go test ./pkg/basecamp/catalog/... ./scripts/gen-catalog/... → pass
  • gofmt → clean
  • golangci-lint v2.13.2 → go module (CI's lint scope, working-directory: go) 0 issues. Note: scripts/gen-catalog is a separate module CI does not lint; running v2.13.2 against it with the go config surfaces only a pre-existing gosec G306 on the untouched os.WriteFile(..., 0o644) in run() (verbatim on main) — deliberately not touched here (out of scope, would change generated-file perms).

Review request

@coworker-70 — please review (tag-pipeline owner): (1) confirm which generator this read matches semantically, (2) confirm the not_patch note replacement text and the entry-criterion framing, and (3) coordinate the paired landing — the not_patch edit and this derive must land together so neither side is briefly lying.

Draft — do not merge.


Summary by cubic

Derives gen-catalog's emission bound from spec/generated-verbs.json instead of a hardcoded five-verb map, so the catalog and the six SDK generators share one source of truth. The generated catalog stays byte-identical because patch was already removed from the file and no operation uses it.

Behavior

Side effects

  • Adding a verb to spec/generated-verbs.json now automatically widens gen-catalog's accepted surface; the entry criterion is whether all six runtimes can serve the verb, not whether the API models it.
  • Updates the stale header comment in scripts/gen-catalog/main.go and the not_patch note to match.

Written for commit ce3ae13. Summary will update on new commits.

Review in cubic

gen-catalog carried its own five-verb allow-list — a known copy of the
same fact six SDK generators already read from spec/generated-verbs.json,
which is exactly the divergence #925 was. Replace the hardcoded map with a
permissive derive from that single declaration (basecamp-sdk#926):

- loadGeneratedVerbs reads the file with encoding/json, keeps the
  non-empty/non-whitespace verb strings, and FAILS LOUD on an unreadable
  or unparseable file AND on a file that yields no usable verb (verbs
  absent, [], or all blank). No fallback to a hardcoded default: a silent
  fallback is how a stale superset (e.g. one still naming patch) survives
  the file dropping it, so an empty bound stops the build rather than
  reading as "no restriction." No downstream len==0 short-circuit turns an
  empty bound into allow-everything.
- The read is one isolated function so the #935 switch to a generated Go
  constant is a one-function swap, not a rewrite.
- Build takes the bound as a parameter; the exclusion-based path-item slot
  discovery and the fail-loud-on-out-of-bound-verb behavior are unchanged,
  now bounded by the file rather than the literal.

Post-#932 the file no longer lists patch, so the derived bound differs
from the old literal only by patch (unused) and catalog.json regenerates
byte-identical.

Also fixes the stale header comment (it still named the five hardcoded
verbs) and updates the not_patch note in generated-verbs.json, whose last
sentence described gen-catalog as carrying a superset — now false, since
the bound is identical by construction.
@github-actions github-actions Bot added the spec Changes to the Smithy spec or OpenAPI label Sep 19, 2026
@jeremy

jeremy commented Sep 19, 2026

Copy link
Copy Markdown
Member Author

Reviewed as the SDK side of this. The code and the tests are right — this is the contract we agreed, implemented exactly: permissive unmarshal, non-empty/non-whitespace filter, fail loud on unreadable and on no-usable-verb, no fallback to a hardcoded default, and no len==0 short-circuit downstream that could read an empty bound as "no restriction." The tests assert on error content rather than just a non-nil error, and TestLoadGeneratedVerbsNoUsableVerbs covers absent / [] / all-blank separately. That is the standard #932 landed on, and it is met here.

One design choice worth naming so nobody undoes it later: defaultGeneratedVerbs is a constant rather than a flag or a run() parameter, unlike the other three paths. That is the right call and it should say so — an overridable path is what produced two of #933's findings, where the gate validated one file while three generators read another. Keeping it un-overridable means the bound cannot be pointed somewhere the generators aren't looking.

Three defects, all in prose, one of them user-facing.

1. The comment claims an upstream validator that does not exist. loadGeneratedVerbs's doc says:

spec/check-generated-verbs is the ONE validator of the declaration and a prerequisite of every generate target, so a malformed file never reaches here

None of that holds. #933 was closed unmerged — check-generated-verbs exists nowhere on main or on this branch, and nothing in the Makefile references it. Worse, "a prerequisite of every generate target" is the precise property #933 proved unreachable: placing that gate reached eight generation entry points across two review rounds before it was abandoned, ending at make -j scheduling it concurrently with the checks it was meant to precede.

The narrowness is still correct, but its justification here is backwards. Nothing guarantees the file is well-formed before this function sees it — that is exactly why fail-loud is the contract, rather than a reason the read can be relaxed. As written, the next reader concludes there is upstream protection and may loosen this. Suggest replacing with something like: "This adds no BOM/stream/shape opinions of its own — not because something upstream validates the file (nothing does; see #933), but because a seventh JSON opinion is what #933 showed cannot define validity for the other six. It asserts one fact about this process's own state: I obtained a usable set to bound on, or I stop."

2. The error message sends the user to a command that doesn't exist. The no-usable-verb error ends with Run 'make check-generated-verbs'. There is no such target. Someone reading that message is already debugging a broken build; pointing them at a nonexistent command costs them a real detour. Drop the sentence, or point at spec/generated-verbs.json itself, which is the file they need to look at.

3. basecamp-sdk#926 is the wrong citation. It appears twice, for the claim that this generator and the six "can never disagree about the surface." #926 is the merged PR that shipped the embedded catalog — it is not about duplicated policy. The argument belongs to #935 (language-independent policy transcribed once per language), with #925 for the original failure. As cited, a reader chasing the reasoning lands on the catalog's own shipping PR.

The spec/generated-verbs.json edit is correct and matches what we agreed, and rewriting the main.go:47 header to name the declaration as the source rather than restating the verbs is the right fix for a claim that was already stale on main after #932 dropped patch.

Worth stating in the PR body if it is not there already: the derived bound differs from the old hardcoded set only by patch, no operation in the spec uses it, so catalog.json should be byte-identical and make catalog-check green. If that assumption breaks, it is a finding rather than a rebase artifact.

- Cite #935 (policy transcribed once per language), not #926 (the merged
  catalog PR), for the 'generators can never disagree' argument; keep #925
  as the original failure.
- Fix the loadGeneratedVerbs rationale: no upstream validator exists (#933
  closed unmerged and proved a 'prerequisite of every generate target'
  unreachable), so fail-loud IS the contract — not a reason to relax the read.
- Drop the nonexistent 'make check-generated-verbs' target from the
  no-usable-verb error; point at the verbs array in the file instead.
- Say why defaultGeneratedVerbs is a constant, not a flag (an overridable
  bound produced two of #933's findings).
@jeremy

jeremy commented Sep 19, 2026

Copy link
Copy Markdown
Member Author

Addressed the review in ce3ae13c:

  1. Fixed — loadGeneratedVerbs no longer claims a spec/check-generated-verbs validator (none exists; One validator, six readers: stop the generators disagreeing about the shared declaration #933 closed unmerged and proved a "prerequisite of every generate target" unreachable under make -j). It now states the true, stronger reason: nothing guarantees the file is well-formed before this function sees it, so fail-loud is the contract — not a reason to relax the read.
  2. Fixed — dropped the nonexistent make check-generated-verbs command from the no-usable-verb error; it now points at the verbs array in the named file.
  3. Fixed — the "generators can never disagree about the surface" argument now cites Language-independent policy is transcribed once per language #935 (with Generators iterate a five-verb list, so an operation on any other HTTP method is silently dropped from every SDK #925 as the original failure), not Ship an authoritative embedded tool Catalog distilled from the SDK's own spec #926 (the merged catalog-shipping PR). Both occurrences.
  4. Added — the defaultGeneratedVerbs comment now says why it is a constant, not a flag or run() parameter (an overridable bound is what produced two of One validator, six readers: stop the generators disagreeing about the shared declaration #933's findings — the gate validating one file while the generators read another).
  5. PR body — the "Byte-identical catalog" section already covered the patch-only difference; added the "if catalog-check ever goes non-green it is a finding, not a rebase artifact" framing.

make catalog-check still byte-identical (265 ops / 32 tags); gofmt / build / vet clean. Ready for re-check.

@jeremy

jeremy commented Sep 19, 2026

Copy link
Copy Markdown
Member Author

Re-checked at ce3ae13. All three defects are fixed, verified against the tree rather than the summary:

  • No reference to spec/check-generated-verbs survives anywhere in main.go, and the rewritten rationale states the reason the right way round — nothing upstream guarantees well-formedness, which is why fail-loud is the contract.
  • The no-usable-verb error now points at the verbs array in the named file instead of a target that does not exist.
  • #926 is gone (0 occurrences); #935 carries the can-never-disagree argument in all three places, with #925 kept for the original failure.

catalog.json does not appear in the diff at all, which is the byte-identical claim confirmed structurally rather than asserted — the derived bound differs from the old literal only by patch, and nothing uses it.

Two things left, both procedural rather than about the code.

This is still a draft. Draft status does not gate merging, it gates reviewing — it delays the Codex and Copilot cycle, which is the thing that takes wall-clock here. Worth marking ready for review so the bots start; mergeStateStatus is BLOCKED largely because of it, alongside 11 checks still running with none failing.

There is no companion PR to wait for. The spec/generated-verbs.json not_patch edit is in this PR — it is the third file in the diff. That was the point of consolidating: scripts/gen-catalog/ lives inside this repo, so the code change and the two prose corrections it falsifies belong in one commit and land together. Nothing is pending on a second PR from anyone.

@jeremy
jeremy marked this pull request as ready for review September 19, 2026 08:23
Copilot AI balanced review requested due to automatic review settings September 19, 2026 08:23
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 19, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-19T08:25:15.935410Z ce3ae13 Draft marked ready
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The PR description and implementation conflict on whether an upstream generated-verbs validator exists.

Get a fresh assessment by requesting another Copilot review.

Review effort: Balanced
Findings: 1 Low severity

Open (1)
What changed in this PR

Derives the catalog generator’s HTTP verb bound from the shared declaration instead of a hardcoded list.

Changes:

  • Adds fail-loud loading of generated verbs.
  • Passes the derived bound into catalog construction.
  • Adds loader tests and updates documentation.

[!TIP]
If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.

File Description
spec/​generated-verbs.json Documents catalog derivation consequences.
scripts/​gen-catalog/​main.go Loads and enforces the shared verb bound.
scripts/​gen-catalog/​main_test.go Tests loading and fail-loud behavior.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread scripts/gen-catalog/main.go
@jeremy
jeremy merged commit 16fb4ce into main Sep 20, 2026
50 checks passed
@jeremy
jeremy deleted the catalog-embed-derive-generated-verbs branch September 20, 2026 06:07
@jeremy jeremy mentioned this pull request Sep 20, 2026
jeremy added a commit that referenced this pull request Sep 20, 2026
11 version files and 8 lockfiles, written by scripts/bump-version.sh.
MIGRATING.md's '# Unreleased' section is promoted to '# v0.20.0'.

Thirteen commits since v0.19.0 (2026-09-16), the substantial ones being
the #925 chain — #931 finding operations by exclusion so no HTTP verb can
drop one in silence, #932 stopping a refusal from destroying generated
output, and #938 deriving gen-catalog's emission bound from the shared
declaration — plus #926's embedded tool Catalog, #930's Automation split,
#924's event-feed body validation at the wrapper's decode, and #937's
anyio bump for CVE-2026-63374.

Ran the release's own preflight here rather than discovering it at tag
time: promote-migrating --check passes, rs-publish-check packages and
dry-run-uploads 0.20.0 clean, and all four lockfile freshness assertions
hold.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

spec Changes to the Smithy spec or OpenAPI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants