Repository navigation
Ship an authoritative embedded tool Catalog distilled from the SDK's own spec - #926
Conversation
Sensitive Change Detection (shadow mode)This PR modifies control-plane files:
|
There was a problem hiding this comment.
🟡 Changes recommended
The generator accepts blank tags, allowing operations to be emitted without a usable domain.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Adds a generated, embedded Go catalog combining OpenAPI operations with behavioral metadata.
Changes:
- Adds the catalog generator, artifact, loader, and tests.
- Integrates generation and drift checks into Make and CI.
- Exposes typed operation lookup and grouping APIs.
[!TIP]
If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or rungh pr ready --undo.
Click "Ready for review" or rungh pr readyto reengage.
File summaries
| File | Description |
|---|---|
scripts/gen-catalog/main.go |
Generates the catalog from source artifacts. |
scripts/gen-catalog/main_test.go |
Tests joins, references, and invariants. |
scripts/gen-catalog/go.mod |
Defines the generator module. |
go/pkg/basecamp/catalog/catalog.go |
Provides the public catalog API. |
go/pkg/basecamp/catalog/catalog_test.go |
Tests loading and catalog integrity. |
go/pkg/basecamp/catalog/catalog.json |
Contains the generated embedded catalog. |
Makefile |
Adds generation and drift-check targets. |
go.work |
Registers the generator module. |
.github/workflows/test.yml |
Enforces catalog freshness in CI. |
Review details
- Files reviewed: 8/9 changed files
- Comments generated: 1
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
aeb6f28 to
e117950
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: aeb6f28528
ℹ️ 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 Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
e117950 to
211f313
Compare
…own spec Downstream tool-catalog consumers (basecamp-mcp-server and any other) vendor openapi.json + behavior-model.json and re-join them at startup to derive their per-operation tool surface. That snapshot drifts and duplicates a join the SDK is best placed to do once, at the source. This adds a distilled, versioned, embedded Catalog to the Go module — the operation-shaped sibling of the existing url-routes.json embed. scripts/gen-catalog joins openapi.json (identity, wire shape, request-body schemas) with behavior-model.json (readonly/idempotent/retry/pagination/write traits) into one self-contained go/pkg/basecamp/catalog/catalog.json: request-body $refs inlined, x-go-* codegen noise stripped, 396 KB versus the 1.1 MB raw openapi.json. The new pkg/basecamp/catalog package embeds it and exposes a typed loader (catalog.Load, Operations) over Catalog/Operation structs carrying everything a per-operation tool needs. Consumers read it from the pinned dependency instead of vendoring anything. The join is strict both ways (every operation in both files, exactly one tag), matching the toolkit loader's contract, so a torn regeneration fails loudly. A make catalog target regenerates the artifact and a make catalog-check gate — wired into check-targets and its own CI step — fails when the committed catalog is not byte-identical to a fresh distillation, matching the url-routes/behavior-model posture. Destructive is carried as a tri-state trait sourced only from the behavior model, which does not declare it yet, so it is absent today and a consumer keeps bridging from the action name. It is deliberately not derived from the HTTP method: Basecamp serves nine reversible toggles over DELETE (UncompleteTodo, UnpinMessage, DisableTool, ...) and one genuinely destructive operation (TrashRecording) over PUT, so a method rule would be wrong both ways. Destructive belongs in a curated Smithy trait; the loader surfaces it the moment the model grows one.
211f313 to
5ec145a
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5ec145aff3
ℹ️ 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".
| generate: | ||
| @$(MAKE) smithy-build | ||
| @$(MAKE) behavior-model url-routes provenance-sync | ||
| @$(MAKE) behavior-model url-routes catalog provenance-sync |
There was a problem hiding this comment.
Serialize catalog generation after the behavior model
When make -j generate follows a Smithy change that affects behavior traits, this sub-make may schedule catalog concurrently with behavior-model; the catalog generator can therefore read the previous behavior-model.json before the atomic replacement completes and leave catalog.json stale, despite the surrounding comment promising ordered phases. Put catalog in a subsequent sub-make or add an explicit dependency so it cannot start until behavior-model finishes.
AGENTS.md reference: AGENTS.md:L123-L132
Useful? React with 👍 / 👎.
- 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).
) * Derive gen-catalog's emission bound from spec/generated-verbs.json 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. * Correct gen-catalog derive comments and no-verb error - 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).
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.
What
Ships an authoritative, embedded tool Catalog in the Go module — the
operation-shaped sibling of the existing
url-routes.jsonembed. Downstreamconsumers generate their tool catalog directly from the pinned SDK dependency
instead of vendoring
openapi.json+behavior-model.jsonand re-joining them.scripts/gen-catalogdistills one self-containedgo/pkg/basecamp/catalog/catalog.jsonby joining, at the source:
params, request-body JSON schema (component
$refs inlined,x-go-*codegennoise stripped)
readonly/idempotent/retry/pagination/writetraitsResult: 396 KB vs the 1.1 MB raw
openapi.json, self-contained (0 residual$refs), 265 operations across 27 tags.Public API (
github.com/basecamp/basecamp-sdk/go/pkg/basecamp/catalog)This is a strict superset of what
basecamp-mcp-server'sdomains.jsonneeds peroperation, so the cutover is a mechanical field map. Product-specific concerns stay
with the consumer: tool prefix, tag→domain grouping, and the action-name spelling
(
snake_case(Operation.ID)).The design deliberately prefers a distilled artifact over embedding the raw
1.1 MB
openapi.json: smaller, focused, stable, and self-contained (nocomponentssection needed at read time). It follows the repo's existing
url-routes.jsonprecedent — a distilled
openapi.jsonjoin embedded beside the code that reads it.Generated, not hand-maintained + drift gate
make catalogregenerates the artifact (also wired intomake generateafterurl-routes).make catalog-checkruns the generator's unit tests, regenerates to a temp file,and fails unless the committed
catalog.jsonis byte-identical. Wired intocheck-targetsand its own CI step in thetest-gojob — becausemake checkmembership is not enforcement in this repo (no CI job runs the full
make check),matching the
url-routes-check/behavior-model-checkposture.mirroring the toolkit loader's contract so a torn regeneration fails loudly.
scripts/gen-catalogis a stdlib-only module ingo.work(likescripts/check-wrapper-drift), sogo runneeds no network.The
destructivetrait gap (important)behavior-model.jsoncarries nodestructivetrait today — it has onlyreadonly,idempotent,pagination,retry,write. The Catalog carriesDestructive *boolsourced only from that (absent) trait, so it isnilonevery operation today and a consumer keeps bridging from the action name
(
mcp/catalog.BridgeDestructive). The loader surfaces the real trait the moment themodel grows one, retiring the bridge.
It is deliberately not derived from the HTTP method. Empirically, neither method
nor name is authoritative here:
UncompleteTodo,UnpinMessage,UnspotlightRecording,DeprioritizeAssignment,DisableTool,DisableOutOfOffice,DisableProjectClients,DisableCardColumnOnHold,ResumeQuestion. "DELETE == destructive" would misclassify all nine.TrashRecording, is a PUT — a method rulemisses it.
So
destructiveis a curated semantic judgment that belongs in the Smithy model,not a derived rule. Smallest upstream fix (a focused follow-up, kept out of this PR
to stay additive and clear of in-flight spec work):
basecampDestructivetospec/basecamp-traits.smithy.spec/basecamp.smithy(the semantic judgment — trash/delete/purge/archive that lose or hide data, not
reversible toggles).
scripts/generate-behavior-model:destructive: (if has_trait("basecamp.traits#basecampDestructive") then true else null end).Then
make behavior-model+make catalogflow the trait through with zero codechange here — the plumbing already reads it — and
basecamp-mcp-server'sBridgeDestructiveheuristic and its tripwire test retire.Coordination
Built on
origin/main. Additive to the generate pipeline; does not touchscripts/check-required-tags.rb,spec/overlays/tags.smithy, or the recordingsretag (in flight on
sdk/tag-events-recordings, #922). Once #922 merges, thiscatalog should be regenerated (
make catalog) so it reflects the correctedRecordings/EventFeed tags — and consumers get the corrected tags for free from the
pinned dependency, no consumer change needed.
Includes all 265 operations, including the 3 binary-upload operations
(
CreateAttachment,CreateCampfireUpload,UpdateAccountLogo) flagged viaBodyMediaType(they carry no JSON body). A consumer that wants only JSON-body/bodylessoperations filters
BodyMediaType == "".Verification
go build ./...(go module),go vet ./...,gofmtcleango test ./pkg/basecamp/catalog/andgo test ./scripts/gen-catalog/passmake catalog-checkgreen; regeneration is byte-identical; drift gate verifiedto fail on a mutated artifact
Summary by cubic
Ships an authoritative, embedded tool Catalog in the Go module so downstream consumers get all operations and traits from one self-contained file instead of vendoring and re-joining
openapi.jsonandbehavior-model.json.go/pkg/basecamp/catalogwith a typed loader (Load,Operations) and aCatalog/Operationmodel covering method, path, tags, readonly/idempotent traits, retry, pagination, write semantics, params, and body schemas with$refs inlined.scripts/gen-catalog, a stdlib-only generator wired intogo.work, that distills a 396 KBcatalog.json(265 operations, 27 tags) from the two model files.make catalogandmake catalog-check; the check fails unless the committed artifact is byte-identical to a fresh generation and is wired intocheck-targetsplus a dedicated CI step.Behavior notes
spec/tracking-issues.yml).Operation.Destructiveisniluntil the behavior model declares the trait; it is not derived from the HTTP method because nine DELETE operations are reversible toggles and one destructive operation is a PUT, so consumers keep bridging from action names until then.Written for commit 5ec145a. Summary will update on new commits.
Update: catalog framed as the tag view, generation-time invariants hardened
Per review from the #922/#925 tag-pipeline owner:
names.tomloverrides before the tag. Package/type docs updated to say so plainly.gen-catalogrefuses to emit from a document with any ≠1-tag operation.@http.methodis free-form, so openapi.json can carry HEAD/OPTIONS/TRACE ops no client exposes.gen-catalogwalks each path item by exclusion (skipping$ref/summary/description/servers/parameters/x-*, the same fail-closed shape as Tag the recordings operations as their own domain, and require a tag on every operation #922's gate) and fails loud, naming basecamp-sdk#925, on any operation slot whose verb is outside the generated set — rather than emitting a phantom op that would break the downstream cutover.generate-url-routes; adding a seventh bare copy would make Generators iterate a five-verb list, so an operation on any other HTTP method is silently dropped from every SDK #925 a seven-place fix. No shared source or Go constant exists to derive/import from, so the restated set carries a canonicaltracked in #925claim and a registered row inspec/tracking-issues.yml— the known-defect gate fails when Generators iterate a five-verb list, so an operation on any other HTTP method is silently dropped from every SDK #925 closes, dragging this copy back into view.catalog.jsonis unchanged (byte-identical) — the guards are inert but in place.New generator tests cover each invariant: non-generated-verb rejection (#925), blank/multi/zero-tag rejection, and structural-path-key exclusion.
Inherited CI red (not this change): main is red at
ebe5fd5onKnown-defect tracking issues are openbecause #922 added atracked in #925comment toscripts/check-required-tags.rbwithout registering its row; #927 is the one-row fix. Every open PR inherits it. This PR's own#925registry row is consistent (registered count = swept count = 1).