Skip to content

chore(ci): auto-generate SDK code for standard new spec operations - #71

Merged
ericviana merged 1 commit into
mainfrom
eric/operation-scaffolding
Aug 4, 2026
Merged

ericviana merged 1 commit into
mainfrom
eric/operation-scaffolding

Conversation

@ericviana

Copy link
Copy Markdown
Member

What changed

The api-sync patcher (.api-sync/sync.py) previously classified every spec operation with no corresponding SDK method as needs-human, even for plain CRUD endpoints that fit the SDK's existing conventions exactly. This PR adds a new operation-insert change kind that auto-generates the method(s), TypedDicts, and spec-map entries for those, and narrows needs-human to operations that genuinely need a person.

Operation coverage is derived, not hand-curated

A new reconcile_operations check walks every operation in the current spec (state-based, like the existing reconcile_enums/reconcile_types, not a diff against the old snapshot) and asks: does any resource module already issue this exact (HTTP verb, path template) request? Coverage is read directly off each resource module's own call-site f-strings (discover_resource_modules), so it never needs a hand-maintained map the way spec-map.json's schema/enum entries do.

STANDARD vs NON-STANDARD boundary

An uncovered operation is STANDARD (auto-generated) only if all of these hold:

  • Resource match: the path resolves to exactly one existing resource module, matched by its own existing path prefix (either the plain collection prefix, e.g. .../partner-fees, or the "item" prefix up to the last path param, e.g. .../webhook-endpoints/{id}, which is what lets a new sub-action like .../{id}/secret attach to the same resource as a sibling delete(id)). Zero matches or more than one is NON-STANDARD.
  • JSON only: request and response content, if present, must be application/json only (no multipart, binary, streaming, etc).
  • Plain object/array shape: request/response schemas must resolve to a plain JSON object, or a top-level array of scalars/an already-mapped $ref. oneOf/anyOf anywhere is NON-STANDARD.
  • Every property resolvable: scalars via SCALAR_TYPE_MAP, $ref'd properties only if spec-map.json already maps that schema 1:1, enum-constrained properties only if spec-map.json already maps that exact (schema, property) to a Literal. Anything else (an inline enum with no mapped Literal, a $ref with no existing map entry, a nested inline object) is NON-STANDARD.
  • No name collision: the derived method name (get/get_<suffix>/list/create/update/delete, derived from the HTTP verb and path shape) must not already exist on the target resource class.

Every NON-STANDARD case gets a precise reason string, e.g. "unsupported content type(s): ['multipart/form-data']", "no existing resource matches this path's prefix", "schema uses oneOf, which is unsupported".

Generated code is text-spliced in the same style as the existing enum/property appliers, then run through ruff format + ruff check --fix --select=I so it lands already canonical (no hand-tuned line wrapping needed).

Bump and CI workflow

operation-insert applies now count toward a minor bump, same as enum additions. The api-sync.yml workflow already maps bump == "minor" to a feat: commit/PR prefix ({"minor": "feat", "patch": "fix"}.get(bump, "chore")), so no workflow change was needed for a real future operation-insert to land as feat:.

--check now covers operations

--check previously never looked at operation coverage at all (only --apply's old-vs-new path-key diff did, and only for brand new path keys). It now fails on any pending operation-insert or NON-STANDARD operation gap, state-based like every other reconcile check. This surfaced a real, previously invisible pre-existing gap -- the RFI endpoints (GET/POST .../rfi) have no resource module at all -- which is now recorded as a kind=operation entry in unmodeled.json (new kind, same "honest ledger with a reason and owner" pattern as the existing kinds) rather than silently generated or silently ignored.

Golden-test evidence

tests/test_api_sync_golden.py (new) operates on a scratch copy of the real repo, not a synthetic fixture:

  • Deleted WebhookEndpointsResource(Sync).get_secret() (GET, single path param, inline/unnamed response schema) and .create() (POST, request+response both $ref'd to named, spec-mapped schemas: WebhookEndpointIn/WebhookEndpointOut), plus their TypedDicts and the two spec-map entries.
  • Ran --apply against the repo's own committed spec snapshot: both methods were regenerated. Verified the regenerated get_secret's route, verb, and GetWebhookEndpointSecretResponse type match the original exactly, and same for create's route, verb, CreateWebhookEndpointInput/CreateWebhookEndpointResponse.
  • Verified the two spec-map entries (WebhookEndpointIn -> CreateWebhookEndpointInput, WebhookEndpointOut -> CreateWebhookEndpointResponse) were restored.
  • Ran pyright, mypy, and pytest against the regenerated tree: all green.
  • Ran --apply a second time: no-op (applied: [], bump: null), confirming idempotency.
  • Ran --check on the regenerated tree: green.
  • A synthetic fixture operation with a multipart/form-data request body correctly routes to needs-human with a reason naming multipart/form-data explicitly (verified against the same golden repo, using a path that resource-matches cleanly so it's specifically the content-type check, not resource matching, being exercised).

Local gauntlet run

ruff format --check, ruff check, pyright, mypy, pytest (233 existing + 10 new), sync.py --validate-map, sync.py --check, check_contract.py, and the determinism proof from api-sync-check.yaml (apply twice against the committed snapshot, diff the two results and diff against the untouched tree) all pass locally.

Claude-Session: https://claude.ai/code/session_01F1stiNzuNtJXoXtiW9ZCbs

Teaches the api-sync patcher a new operation-insert change kind. Today,
any spec operation with no corresponding SDK method is always needs-human,
even for ordinary CRUD endpoints that fit the SDK's existing conventions.

Adds a state-based operation-coverage check (reconcile_operations):
resource modules are discovered directly from their own call-site
f-strings (verb + path template), so coverage never needs a hand-curated
map. An uncovered operation is classified STANDARD (JSON in/out, request
and response resolve to a plain object or scalar/ref array, and the path
matches exactly one existing resource module by prefix) or NON-STANDARD,
with a precise reason (unsupported content type, no/ambiguous resource
match, oneOf/anyOf, unmappable property type). STANDARD operations get a
generated sync/async method pair, TypedDicts synthesized from the spec
schema, and spec-map entries for any newly referenced named schemas.

--check now fails on a pending operation-insert (it did not check
operation coverage before). Applied operation-inserts bump minor, so the
existing api-sync workflow's bump-to-prefix mapping (minor -> feat) covers
them automatically, no workflow change needed.

A pre-existing, previously invisible gap (the RFI endpoints have no
resource module at all) is now caught by this check; recorded as a new
unmodeled.json kind=operation entry rather than silently patched over.

Claude-Session: https://claude.ai/code/session_01F1stiNzuNtJXoXtiW9ZCbs
@BernardoSM

Copy link
Copy Markdown
Collaborator

✅ Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
✅ Code Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@ericviana
ericviana merged commit a88b066 into main Aug 4, 2026
8 checks passed
@ericviana
ericviana deleted the eric/operation-scaffolding branch August 4, 2026 15:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants