Skip to content

Apply the type-evolution policy: non_exhaustive on what HEY answers - #166

Merged
jeremy merged 2 commits into
rustlb-w4-api-governance-policyfrom
rustlb-w4-non-exhaustive
Sep 10, 2026
Merged

jeremy merged 2 commits into
rustlb-w4-api-governance-policyfrom
rustlb-w4-non-exhaustive

Conversation

@jeremy

@jeremy jeremy commented Sep 10, 2026 •

Copy link
Copy Markdown
Member

Tracking card: hey-sdk Rust: public API governance. Second of two, stacked on #163 (base is its branch; GitHub retargets to main when it merges). Merge order: #163, then this PR. It depends on #163 for the lint table the tests now satisfy and the policy text it applies; both are kept rebased on main and converged together. This is the decision that has to land before the first crates.io publish: adding #[non_exhaustive] later is itself a breaking change.

What is #[non_exhaustive] now, and why. The rule from #163, applied: what the crate decodes and hands back is read, never built; what a caller sends stays literal.

  • Generated (rust/generator/src/model.rs mark_request_side, emit/types.rs): the generator walks out from every request body to everything it mentions, however deep, and marks those schemas request-side; every other struct schema is emitted #[non_exhaustive]. On today's model that is 51 request-side structs (every *RequestContent and the payloads they carry — MessagePayload, ContactPayload, StickyPayload, …) and 86 response-side ones (Mailbox, Posting, Topic, Contact, Calendar, the *ResponseContent envelopes, the error bodies, …); no schema is on both sides. The *Params structs are untouched. Tests: model.rs (a body, what it mentions through a list, and a shape shared with a response all stay request-side; the rest do not), emit/types.rs (the attribute is emitted for one and not the other), and a generator fixture running the whole thing end to end.
  • Hand-written results: WorkflowSummary, CalendarChanges, RecordingChanges, DeletedRecording, DeletedCalendar, PostingChanges, SearchResults, ContactConflict, ListedCalendar, CalendarList, services::extenzions::Extenzion, url::Match.
  • Runtime: client::Response, FormResponse, oauth::Token, and what the hooks hand back — RequestInfo, RequestResult.
  • Open enums (their variant sets are HEY's to grow): ErrorCode, route::Pagination, route::ParamKind, BoxKind, StickySize, BubbleUpSlot, RepeatFrequency, CountdownUnit, TimeFormat.

Left constructible, on purpose: OperationInfo (Operation::info takes one from a caller describing its own form-backed call); Route, RouteParam, Retry (a caller may write a route for a path the model lacks and hand it to Client::operation — #154's tests/retry_policy.rs does exactly that, which is how this line got drawn); Config, the resilience configs, ExchangeRequest, RefreshRequest, ServerMetadata, Pkce, CachedResponse, the *Cursor structs; and the closed enums ClearanceStatus, OccurrenceScope, RepeatUntil, ParamRole. A field added to any of those is a 0.MINOR.

What is and isn't breaking afterward. A field added to a response-side type or a variant added to an open enum is a 0.x.PATCH, provided what HEY already sends still decodes (a required field with no decoding default is not additive). A field added to a request-side type is a 0.MINOR. cargo semver-checks against #163's head reports exactly the two checks this change is: struct_marked_non_exhaustive and enum_marked_non_exhaustive, nothing else.

Migration (MIGRATING.md, new at the repo root, Rust section for the first release after 0.30.0): literal construction of a response-side type stops compiling, ..Default::default() included — build with T::default() and set fields; a match over an open enum needs a _ arm; reading, methods, Clone, PartialEq, serde are untouched. The crate's own tests show the shape: tests/recordings.rs builds a Recording through default(); tests/form.rs reads a FormResponse back from a mock instead of building one; tests/services_extenzions.rs and tests/services_workflows.rs compare fields.

Enums from the model. None today (openapi.json declares no enum), so the generator invents none; the Unknown(String) shape is policy in #163 and lands as emit the day the model declares one.

The breaking label, and why it is the instrument. api-compat-rust runs cargo semver-checks against the base commit and fails on any major-class change; its if: skips the job when the pull request carries the breaking label, the same escape hatch the Go job has, and the crate's own policy text names for a pre-1.0 break (README, Versioning). The label is on. The alternative the job's comment offers — bumping Cargo.toml to 0.31.0 in the PR so the checker judges it a permitted major — is wrong here: the Go module and the Rust crate share one version and one tag, and the version moves when a release is cut, not inside a feature PR. The break itself is intended and is the whole point: pre-1.0, 0.30 → 0.31 is the major, and #[non_exhaustive] has to be on the response types before the first crates.io publish, because adding it afterward is itself the same break against a public baseline. cargo semver-checks --baseline-rev <#163 head> on the current head reports exactly struct_marked_non_exhaustive and enum_marked_non_exhaustive, nothing else, so the label excuses only what this PR sets out to do. main is not branch-protected, so a skipped job holds nothing up.

Local gate on Rust 1.98.1, at the current head (rebased with #163 onto main after #165 and #170): make -C rust check (fmt, clippy --all-targets -D warnings, test — 48 suites, doc -D warnings, --no-default-features, examples, deny, publish dry run) exit 0, rs-check-drift exit 0, conformance-rs 195/195; cargo check -p hey-sdk on 1.88.0 with --all-features and --no-default-features both build; cargo semver-checks as above.

Review. Adversarial Codex CLI pass done (gpt-6-astra at xhigh, read-only) over the hand-written diff: one finding, folded in (the fixture test says where a result without Default comes from). Copilot recommended approval; no inline threads. Babysitting to convergence.

Copilot AI balanced review requested due to automatic review settings September 10, 2026 06:09
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 10, 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-10T07:42:37.600668Z e8c02c1 New commits
ℹ️ 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.

🟢 Approval recommended

The classification matches all 51 request-side and 86 response-side generated structs, with focused generator and migration coverage.

Pull request overview

Applies the Rust API-evolution policy established in #163 by making response types and open enums non-exhaustive while preserving request-type construction.

[!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.

Changes:

  • Classifies generated schemas by request-body reachability.
  • Marks generated and handwritten response types and open enums #[non_exhaustive].
  • Updates tests and migration documentation for the resulting API changes.
File summaries
File Description
AGENTS.md Clarifies route metadata evolution policy.
MIGRATING.md Documents required consumer migrations.
rust/generator/src/emit/types.rs Emits the non-exhaustive attribute.
rust/generator/src/fixtures.rs Adds end-to-end generation coverage.
rust/generator/src/model.rs Classifies request-side schema reachability.
rust/hey-sdk/README.md Updates public versioning policy.
rust/hey-sdk/src/client.rs Marks raw responses non-exhaustive.
rust/hey-sdk/src/error.rs Opens the error-code enum.
rust/hey-sdk/src/form.rs Marks form responses non-exhaustive.
rust/hey-sdk/src/generated/types.rs Regenerates response-side types.
rust/hey-sdk/src/oauth.rs Marks OAuth tokens non-exhaustive.
rust/hey-sdk/src/observability.rs Opens hook request/result types.
rust/hey-sdk/src/route.rs Opens model-controlled route enums.
rust/hey-sdk/src/services/boxes.rs Opens box kinds.
rust/hey-sdk/src/services/calendar_changes.rs Protects change-feed result types.
rust/hey-sdk/src/services/calendar_events.rs Opens recurrence/countdown enums.
rust/hey-sdk/src/services/calendars.rs Protects calendar list results.
rust/hey-sdk/src/services/contacts.rs Protects conflict details.
rust/hey-sdk/src/services/extenzions.rs Protects extension results.
rust/hey-sdk/src/services/identity.rs Opens time formats.
rust/hey-sdk/src/services/postings.rs Opens posting result types and slots.
rust/hey-sdk/src/services/search.rs Protects search results.
rust/hey-sdk/src/services/stickies.rs Opens sticky sizes.
rust/hey-sdk/src/services/workflows.rs Protects workflow summaries.
rust/hey-sdk/src/url.rs Protects recognized route matches.
rust/hey-sdk/tests/form.rs Tests form responses through actual requests.
rust/hey-sdk/tests/recordings.rs Migrates response construction to mutation.
rust/hey-sdk/tests/services_extenzions.rs Avoids literal response construction.
rust/hey-sdk/tests/services_workflows.rs Compares workflow result fields.
Review details
  • Files reviewed: 26/27 changed files
  • Comments generated: 0
  • Review effort level: Balanced

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

@jeremy

jeremy commented Sep 10, 2026

Copy link
Copy Markdown
Member Author

Adversarial review (Codex CLI, gpt-6-astra at xhigh, read-only) over the hand-written diff: one finding, folded in 0784ea5. MIGRATING advised T::default() plus field assignment for every type that lost literal construction, but Token, client::Response, FormResponse, RequestInfo, RequestResult, DeletedCalendar, DeletedRecording and url::Match have no Default; the note now says where a fixture for each comes from (serde for the wire types, a mocked call for the runtime results, Router::recognize for a match). It also ran its own reachability check over all 137 generated structs and matched the classification; no implementation defects found.

@jeremy
jeremy force-pushed the rustlb-w4-non-exhaustive branch from 0784ea5 to 57c6b22 Compare September 10, 2026 06:44
@jeremy

jeremy commented Sep 10, 2026

Copy link
Copy Markdown
Member Author

Rebased with #163 onto main at 1d3d047; gate green (48 suites, drift, conformance 195/195). Still stacked: merge #163, then this.

Every type the crate decodes and hands back, and every enum whose values
are HEY's to grow, is #[non_exhaustive]; what a caller sends stays
literal. The generator decides by reachability from the request bodies —
a schema a body mentions however deep is the caller's to build, the rest
is read back and only read — so a field the model adds to a response is
an addition and one it adds to a request is a break, as the README says.

The hand-written side follows the same line: the route table, the client
and form responses, what the hooks report, the OAuth token, the parsed
results in the services, and the open enums. OperationInfo, the configs,
the OAuth requests, the cursors and the closed enums stay constructible.

Tests that built a response by hand now read one back or compare fields.
MIGRATING.md says what changes for code outside the crate.
@jeremy
jeremy force-pushed the rustlb-w4-non-exhaustive branch from 57c6b22 to e8c02c1 Compare September 10, 2026 07:38
@jeremy
jeremy merged commit beaf495 into main Sep 10, 2026
26 checks passed
@jeremy
jeremy deleted the rustlb-w4-non-exhaustive branch September 10, 2026 07:44
@jeremy

jeremy commented Sep 10, 2026

Copy link
Copy Markdown
Member Author

Rebased onto #163 at e42e451 (which is main at 5680b25 after #165 and #170); the two commits here applied clean. #165's trace.rs is a private module, so nothing new falls under the policy. cargo semver-checks --baseline-rev e42e451 reports exactly struct_marked_non_exhaustive and enum_marked_non_exhaustive, nothing else.

On the breaking label, so it is not re-argued later: api-compat-rust skips when the label is on (the job's if:, the same as the Go job), and that is the documented instrument for a pre-1.0 major in the README's Versioning section. The other route the job's comment offers, bumping Cargo.toml to 0.31.0 inside the PR, is not taken: the crate and the Go module share one version and one tag, and the version moves when a release is cut. The break is the point of the PR and has to land before the first crates.io publish. main is not branch-protected, so the skipped job does not hold anything. Reasoning is in the body as well.

Gate at e8c02c1: make -C rust check (48 suites) exit 0, rs-check-drift exit 0, conformance-rs 195/195, cargo check on 1.88.0 with --all-features and --no-default-features. Still stacked: merge #163, then this one.

@jeremy jeremy added the rust Pull requests that update rust code label Sep 10, 2026
jeremy added a commit that referenced this pull request Sep 10, 2026
Since v0.30.0 the Rust crate's response types and enums are #[non_exhaustive]
(#166, breaking), the Rust client honors each route's retry policy and the Go
client holds its retry settings as a ceiling over it (#154, #164), operations
carry tracing spans behind a feature (#165), a workflow stage is read out of
the page HEY serves (#169), and the crate publishes to crates.io on the tag
(#162). cargo semver-checks against v0.30.0 asks for the minor.

The bump script still looked for the README dependency line #160 replaced.
The READMEs now show the crates.io requirement, which names the minor, and
the git dependency pinned to the release tag, which names the whole version;
the script moves both and checks each.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking rust Pull requests that update rust code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants