feat!: spec sync 3.23.0 → 3.24.0 (v7.0.0) — close spec-drift #467/#470#472
Conversation
…, content titles The recurring #467 (nightly) / #470 (weekly) issues are two detectors on one real, deterministic upstream drift — but the reporting mechanics amplified the pain. Fix the mechanics without weakening the read-only security model. Nightly (spec-drift.yml): - Comment only when the failing-test-ID set changes (a fingerprint), not every failing night — a deterministic upstream produced identical spam indefinitely. - `spec-drift-ack` / `snoozed` label silences comments while a reconcile is in flight, without closing the tracker. - New report-success job auto-closes the tracker on a green scheduled run (the workflow previously only ever opened/commented). Weekly (spec-sync.yml): - Title leads with a content signal ([content-changed] + path delta + short fingerprint) instead of a bare version — Kalshi changes content in place without bumping info.version, so "3.23.0 → 3.23.0" read as a false alarm. - Supersede stale weekly snapshots so exactly one weekly drift issue stays open instead of one per upstream micro-revision. Scoped to spec-sync-bot so the nightly tracker is never touched. Refs #467, #470 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Direction 1 — less brittle (stop benign churn redding CI against a fast-moving, loosely-versioned upstream): - Additive drift split: a spec-REQUIRED field the SDK lacks still hard-fails (act now); a spec-OPTIONAL field becomes a non-blocking AdditiveOptionalDriftWarning. It deliberately subclasses Warning (not UserWarning) so the nightly `-W error::UserWarning` gate cannot promote benign optional drift to a red build. +3 regression tests lock the contract. - test_schema_coverage batches all unresolved schemas into one failure instead of failing on the first (one-at-a-time reruns on multi-removal drift). Direction 2 — close false-negatives (catch real drift the suite missed): - test_every_spec_endpoint_is_mapped asserts every spec OPERATION (core+perps+perps_scm) is in a METHOD_ENDPOINT_MAP, minus a reasoned _UNIMPLEMENTED_ENDPOINTS allowlist. Catches ADDED endpoints/operations the schema/path-count diff misses — verified against live 3.24.0, which had it caught the two new /margin/* endpoints AND a new GET on an existing /communications/rfqs/.../quotes path that the weekly path diff reported as "Added endpoints: (none)". - kwarg_rename exclusions are now spec-anchored (test_exclusion_map_is_current): the spec-side param must still exist for the endpoint, so a rename can't go stale and silently mask an upstream param removal/rename. Docstring updated to the actual post-#171/#172 behavior and to document why response-side field REMOVAL is intentionally NOT gated: the vendored "Kalshi Trade API Manual Endpoints" spec is a curated subset, so SDK response models are a superset by design; gating removals would red CI on every intentional SDK field. Request-BODY drift already gates removals (_check_model_drift), which is where a rename's dropped old name is caught. Refs #467, #470 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…cements soft-deprecate
Reconciles the vendored specs to upstream 3.24.0 and the hand-written SDK to
match. Kalshi changed some content in place without bumping info.version, so
this also absorbs edits made under the same string.
BREAKING:
- Position-transfer price is now fixed-point dollars (Decimal), not integer
cents. `price_cents` → `price` (OrderPrice / DollarDecimal) on
ApplySubaccountPositionTransferRequest, SubaccountTransfer, and
transfer_position() (sync + async). Pass price=Decimal("0.50") for a 50¢ basis.
The old le=100c cap is gone — OrderPrice guards non-negativity + the $0.0001
tick and leaves the upper bound to the server (matches CreateOrderRequest).
Deprecated:
- exchange.announcements() (sync + async) now emits DeprecationWarning. Upstream
removed GET /exchange/announcements + the Announcement schema in 3.24.0 (404s
live). The method + model are RETAINED pending confirmation the removal is
permanent (transient-drop precedent: #452). Contract bindings dropped;
_SOFT_DEPRECATED_MODELS / soft_deprecated allowlists keep the completeness
suites green without re-mapping a gone endpoint.
Added:
- SubaccountNettingConfig.exchange_index (int, required)
- portfolio.balance() optional exchange_index query param (sync + async)
- ObligationEntry.asset_class (perps SCM / Klear; Literal["Crypto"], required)
Deferred to follow-up feature work (allowlisted in _UNIMPLEMENTED_ENDPOINTS):
- GET /communications/rfqs/{rfq_id}/quotes/{quote_id}
- GET /margin/active_obligations
- GET /margin/settlement_estimate_by_asset_class
Verified: full suite 4142 passed / 311 skipped, ruff clean, mypy kalshi/ clean.
Closes #467
Closes #470
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ReviewI reviewed the full diff (note: this PR is stacked on #471, so the diff currently includes that branch's CI-hardening changes too — my comments below focus on the actual "spec sync 3.24.0" reconcile commit SummaryThis is a well-executed, carefully-documented spec sync. The breaking Issues found1. Stale docs —
This is the one real gap given the PR explicitly calls this a breaking change — worth fixing before merge (or as an immediate fast-follow) so the docs don't actively mislead users into a runtime error. 2. Docstring inconsistency — async Things that look correct (verified, not just trusted from the description)
Nit
No security, performance, or test-coverage concerns beyond the doc drift above — the new |
Follow-up within the 3.23.0 → 3.24.0 reconcile: implement the endpoints that
were recognized-but-deferred in _UNIMPLEMENTED_ENDPOINTS.
- communications.quotes.get_for_rfq(rfq_id, quote_id) (sync + async) —
GET /communications/rfqs/{rfq_id}/quotes/{quote_id} -> GetQuoteResponse
(RFQ-scoped variant of the flat quotes.get).
- klear.margin.active_obligations() (sync + async) —
GET /margin/active_obligations -> GetActiveMarginObligationsResponse
(plural sibling of active_obligation()).
- klear.margin.settlement_estimate_by_asset_class() (sync + async) —
GET /margin/settlement_estimate_by_asset_class ->
GetSettlementEstimateByAssetClassResponse + AssetClassSettlementEstimate.
ObligationEntry.asset_class refactored onto the shared AssetClassLiteral.
Wired into METHOD_ENDPOINT_MAP / PERPS_SCM_* maps + CONTRACT_MAP; removed from
_UNIMPLEMENTED_ENDPOINTS; +20 unit tests. Full suite 4162 passed / 311 skipped,
ruff + mypy kalshi/ clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ReviewReviewed the hand-written diff (skipped the vendored Bug: docstring misattached after inserting
|
Summary
Reconciles the SDK to upstream OpenAPI/AsyncAPI 3.24.0 — the real, deterministic drift behind the recurring #467 (nightly) and #470 (weekly) issues. Vendored specs are re-pinned to 3.24.0 and the hand-written models/resources updated to match; the full contract-drift suite goes green. Closes #467, #470.
Changes
Breaking
price_cents(int, 0–100) →price(Decimal, fixed-point dollars) onApplySubaccountPositionTransferRequest,SubaccountTransfer, andsubaccounts.transfer_position()(sync + async). Upstream renamed the wire fieldprice_cents→price(FixedPointDollars).transfer_position(..., price_cents=50)→transfer_position(..., price=Decimal("0.50")).le=100ccap is gone —OrderPriceguards non-negativity + the$0.0001tick and leaves the upper bound to the server (matchesCreateOrderRequest).Deprecated (soft)
exchange.announcements()now emitsDeprecationWarning. Upstream removedGET /exchange/announcements+ theAnnouncementschema in 3.24.0 (404s live). Per prior decision, the method + model are retained pending confirmation the removal is permanent — upstream has transiently dropped endpoints as publishing glitches before (fix: spec drift — settlement_sources on Event + lifecycle strike fields (#451) #452). Contract bindings dropped;_SOFT_DEPRECATED_MODELS/soft_deprecatedallowlists keep the completeness suites green without re-mapping a gone endpoint.Added
communications.quotes.get_for_rfq(rfq_id, quote_id)(sync + async) —GET /communications/rfqs/{rfq_id}/quotes/{quote_id}, the RFQ-scoped get-a-quote (returnsGetQuoteResponse).klear.margin.active_obligations()(sync + async) —GET /margin/active_obligations→GetActiveMarginObligationsResponse(plural sibling ofactive_obligation()).klear.margin.settlement_estimate_by_asset_class()(sync + async) —GET /margin/settlement_estimate_by_asset_class→GetSettlementEstimateByAssetClassResponse+AssetClassSettlementEstimate(+ sharedAssetClassLiteral).SubaccountNettingConfig.exchange_index(int, required)portfolio.balance()optionalexchange_indexquery param (sync + async) — this drift was invisible to Spec drift 2026-07-06: openapi 3.23.0 → 3.23.0 #470's schema/path diff; only the param-drift test caught it.ObligationEntry.asset_class(perps SCM / Klear;AssetClassLiteral=Literal["Crypto"], required)Test plan
uv run pytest tests/— 4162 passed, 311 skippeduv run pytest tests/test_contracts.py— 665 passed (all 13 drift failures cleared; new endpoints mapped)uv run ruff check .— cleanuv run mypy kalshi/— clean (157 files)scripts/sync_spec.py; reference_generated/models.pyregenerated + auditedNotes for reviewers
price_cents → pricechange is a format change (cents→dollars), not just a rename — hence the major bump and migration note.test_announcementsisskip-marked; the method stays registered in the coverage harness because it still exists.*_for_rfqquote family — now includingget_for_rfq— remains outside the integration coverage harness, matching its existing siblings (accept_for_rfq/delete_for_rfq); the klearMarginResourceis likewise outside the harness by design. No behavior change there.Issue links
Closes #467
Closes #470