feat(client)!: live-verified spec drift batch (#737, #736, #738) + verification harness#756
Open
dougborg wants to merge 1 commit into
Open
feat(client)!: live-verified spec drift batch (#737, #736, #738) + verification harness#756dougborg wants to merge 1 commit into
dougborg wants to merge 1 commit into
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
This PR adds live API verification tooling for spec drift investigations and applies verified OpenAPI/client updates for custom fields, service variants, variant bin locations, and stock transfer quantities.
Changes:
- Adds reusable spec-drift probe and cleanup scripts with ledger tracking.
- Updates OpenAPI schema and regenerated attrs/Pydantic client models for verified drift.
- Adjusts MCP stock transfer request building to stringify quantity at the boundary.
Reviewed changes
Copilot reviewed 22 out of 23 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
docs/katana-openapi.yaml |
Source OpenAPI changes for custom fields, service variants, stock transfer rows, and variant bin location bulk body. |
katana_public_api_client/api/custom_fields/create_custom_field_definition.py |
Regenerated custom field create docs/examples. |
katana_public_api_client/api/variant_default_storage_bin/link_variant_default_storage_bins.py |
Regenerated endpoint body type from single object to list. |
katana_public_api_client/models/__init__.py |
Exports new custom field enum models. |
katana_public_api_client/models/create_custom_field_definition_request.py |
Regenerated request model using custom field enums. |
katana_public_api_client/models/create_custom_field_definition_request_options_type_0.py |
Updates options documentation. |
katana_public_api_client/models/create_service_variant_request.py |
Makes service variant SKU optional/null. |
katana_public_api_client/models/custom_field_definition.py |
Changes custom field definition ID to UUID and field/entity types to enums. |
katana_public_api_client/models/custom_field_definition_list_response.py |
Updates example payload. |
katana_public_api_client/models/custom_field_entity_type.py |
Adds custom field entity type enum. |
katana_public_api_client/models/custom_field_type.py |
Adds custom field type enum. |
katana_public_api_client/models/stock_transfer_row_request.py |
Changes stock transfer quantity wire type to string. |
katana_public_api_client/models_pydantic/_generated/__init__.py |
Exports regenerated custom field enums. |
katana_public_api_client/models_pydantic/_generated/common.py |
Regenerated Pydantic custom field models/enums. |
katana_public_api_client/models_pydantic/_generated/inventory.py |
Regenerated service variant SKU optionality. |
katana_public_api_client/models_pydantic/_generated/stock.py |
Regenerated stock transfer quantity as string. |
katana_mcp_server/src/katana_mcp/tools/foundation/stock_transfers.py |
Converts MCP float quantity input to string for API requests. |
scripts/probe_client_error_handling.py |
Adds live error-handling probe. |
scripts/probe_issue_737.py |
Adds custom field definition live probe. |
scripts/probe_remaining_issues.py |
Adds live probes for remaining linked drift issues. |
scripts/probe_shape_only.py |
Adds validation-only request shape probes. |
scripts/spec_drift_verify.py |
Adds ledger, tagging, listing, planning, and cleanup harness. |
uv.lock |
Updates locked editable MCP package version. |
Comments suppressed due to low confidence (1)
docs/katana-openapi.yaml:10815
- This is another single-
$refallOfwrapper used only to attach descriptions. The spec authoring guide (katana_public_api_client/docs/spec-authoring.md:27-32) requires OpenAPI 3.1$refsiblings for this pattern instead, so please replace the wrapper with a direct$refand siblingdescription.
allOf:
- $ref: "#/components/schemas/CustomFieldType"
description: |
Field input type. Immutable after creation. The live API
enforces this enum at validation time (verified via 422
introspection).
entity_type:
allOf:
- $ref: "#/components/schemas/CustomFieldEntityType"
499aade to
b8c2cfc
Compare
b8c2cfc to
60ece5f
Compare
60ece5f to
18b7d39
Compare
Contributor
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 30 out of 31 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (1)
docs/katana-openapi.yaml:10826
- The PR description says
CustomFieldDefinition.optionsshape verification is deferred/blocked, but this create-request example still presentschoicesas the documented request shape. Please remove or soften this example so the spec does not imply unverified behavior.
options:
choices:
- label: Online
- label: Retail
- label: Wholesale
18b7d39 to
acb6529
Compare
acb6529 to
497a490
Compare
497a490 to
25796e6
Compare
25796e6 to
c20b795
Compare
Contributor
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 30 out of 31 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (1)
scripts/probe_shape_only.py:94
- This probe also relies on hard-coded positive IDs to prevent persistence. If the tenant happens to contain these customer/variant IDs, the request can create an SDT-shaped sales order without a ledger entry; verify the IDs are absent before posting or record and clean up any unexpected success.
common = {
"customer_id": 99999999, # known bad
"location_id": sales_loc,
"sales_order_rows": [
{"variant_id": 99999999, "quantity": 1, "price_per_unit": 1.0}
],
c20b795 to
4dd4a91
Compare
Reusable harness (``scripts/spec_drift_verify.py`` + per-issue probe scripts) for running tagged POSTs against a live Katana tenant and cleaning up afterwards. Every test artifact gets an ``SDT-<date>`` prefix and is recorded in ``/tmp/spec-drift-ledger.jsonl``; a single ``cleanup`` subcommand walks the ledger in reverse and deletes everything (idempotent — already-deleted rows are skipped). Spec-drift findings encoded in ``docs/katana-openapi.yaml``: **#737 — CustomFieldDefinition** - ``id``: ``integer`` → ``string, format: uuid`` (per README example, inferred — couldn't live-verify because our API key lacks create permission on this endpoint, but README is authoritative and the shape is universal across Katana custom-field surfaces). - ``field_type``: ``string maxLength: 50`` → new ``CustomFieldType`` enum (``shortText`` / ``number`` / ``singleSelect`` / ``date`` / ``boolean`` / ``url``). Verified via live-API 422 introspection. - ``entity_type``: ``string maxLength: 50`` → new ``CustomFieldEntityType`` enum with **19 values** (vs README's documented 1). Verified via live-API 422 introspection — the enum spans SalesOrder, SalesOrderRow, ManufacturingOrder*, PurchaseOrder*, StockAdjustment*, Production*, Customer, ProductVariant, MaterialVariant, ServiceVariant. - ``CustomFieldDefinition`` no longer extends ``UpdatableEntity`` because the singleton's ``id`` is a UUID string, not the integer ``BaseEntity`` defines for the rest of the API surface. ``created_at`` / ``updated_at`` are inlined. **#736 — variant_bin_locations + CreateServiceVariant.sku** - ``POST /variant_bin_locations`` request body wrapped in array (``minItems: 1, maxItems: 500``). The endpoint description already said "accepts up to 500 variant storage bin objects" but the schema was a single object. Verified via live API: single-object body returns ``422 must be array``. - ``CreateServiceVariantRequest.sku`` is no longer required. The field is nullable. Verified by creating a service variant via the live API with ``sku`` omitted — server accepted it and returned ``sku: null``. **#738 — StockTransferRowRequest.quantity** - Changed from ``number`` to ``string``. Verified via live-API 422 introspection — sending a JSON number returns ``must be string``. Wire format is a fixed-precision decimal string (e.g. ``"1.0000000000"``). MCP boundary in ``stock_transfers._build_row_requests`` stringifies the pydantic ``float`` input at the call site. **#739 — SalesOrderFulfillment** - No spec change needed. Verified ``sales_order_fulfillment_rows`` IS required on create — local spec is correct, README is stale. Findings deferred to follow-ups: - **#734** ``custom_fields`` shape — live API rejects array-shaped custom_fields with ``must be object`` at both SO and SO-row create paths; dict-shape is accepted. Local READ schemas use structured-array shape, which contradicts this. Read-side verification requires a populated example (tenant has zero ``custom_field_definitions`` and our key can't create them). Sweeping all the READ schemas is broader than this PR. - **#736** ``CustomFieldDefinition`` ``options`` shape, ``id`` type via live API, ``UpdateCustomerRequest.default_billing_id``, ``Operator`` fields — all blocked by either authorization (custom_field_definitions endpoint) or absence of populated fixtures on this tenant. BREAKING CHANGE: ``CustomFieldDefinition.id`` becomes a UUID string (was integer); ``field_type`` / ``entity_type`` become enums; ``CreateServiceVariantRequest.sku`` is no longer required (callers omitting it will continue to work, but ``required`` removal is a schema-level breaking change); ``StockTransferRowRequest.quantity`` becomes a string. The MCP ``stock_transfers`` boundary already stringifies the input float so MCP callers don't have to. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4dd4a91 to
e831397
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two threads in one PR:
1. Reusable verification harness
scripts/spec_drift_verify.py(CLI + ledger) plus four per-issueprobe scripts. Every test artifact created against the live tenant
gets an
SDT-<date>prefix and is recorded in/tmp/spec-drift-ledger.jsonl;cleanupwalks the ledger inreverse and deletes everything (idempotent — already-deleted rows
are skipped, 404 on DELETE is treated as "already gone").
listplancleanupProbe scripts already cover #737 (
probe_issue_737.py),#734/#736/#738/#739 (
probe_remaining_issues.py), shape-onlyintrospection without persisting (
probe_shape_only.py), and theproject
ValidationErrorend-to-end via raw-httpx +KatanaClientparallel paths (
probe_client_error_handling.py).2. Live-verified spec drift fixes
#737 — CustomFieldDefinition
idintegerstring, format: uuidfield_typestring maxLength:50$ref CustomFieldTypeenum (6 values)entity_typestring maxLength:50$ref CustomFieldEntityTypeenum (19 values)allOf UpdatableEntitycreated_at/updated_atUpdatableEntitydefines integeridfromBaseEntity; UUID can't reuse itCouldn't live-verify
idUUID-ness via create cycle — our APIkey returns
403 ForbiddenonPOST /custom_field_definitions(endpoint exists, but the account/scope doesn't grant write). The
README example shows UUID and that's universal across Katana's
custom-field surface.
#736 — variant_bin_locations + CreateServiceVariant
POST /variant_bin_locationsrequest body wrapped in array(
minItems: 1, no upper bound — upstream sources disagree on the cap). Description already said"up to 500 variant storage bin objects" but the schema was a
single object. Verified: single-object body returns
422 must be array.CreateServiceVariantRequest.skuno longer required. Verifiedby creating a service variant without
sku— server returnssku: nullon the resulting variant.#738 — StockTransferRowRequest.quantity
numbertostring. Verified via live 422:sending a JSON number returns
must be string. Wire format isa fixed-precision decimal string (e.g.
\"1.0000000000\").stock_transfers._build_row_requestsstringifies the input
floatso MCP callers don't have to.#739 — closed (no spec change)
Verified
sales_order_fulfillment_rowsIS required on create —local spec correct, README upstream is stale on this. Issue ready
to close on merge.
Findings deferred (per scope decision)
#734 — custom_fields shape divergence: live API rejects
array-shaped
custom_fieldswithmust be objectat both SOand SO-row create paths; dict-shape is accepted. Local READ schemas
use structured-array shape, which contradicts this. Read-side
verification requires a populated example (tenant has zero
custom_field_definitionsand our key can't create them).Sweeping all the READ schemas (Variant / Service / Product / SO /
SO Row / SO Fulfillment / Customer / Supplier / SalesReturn etc.)
is broader than this PR.
#736 — remaining items:
CustomFieldDefinition.optionsshape and the
UpdateCustomerRequest.default_billing_idquestion — blocked by either authorization or absence of populated
fixtures on this tenant.
POST /bin_locations404: the local spec documents thisendpoint but the live router returns
404 Endpoint POST /bin_locations not found. Bin locations are created in theKatana UI only. Removing this from the local spec is a cleanup
worth a follow-up PR (orthogonal to the drift findings here).
BREAKING CHANGE
CustomFieldDefinition.idbecomes a UUID string (was integer)CustomFieldDefinition.field_type/entity_typebecome enumsCreateServiceVariantRequest.skuno longer required (schema-level breaking change; runtime-compatible)StockTransferRowRequest.quantitybecomes a string (MCP boundary stringifies the input)Test plan
uv run poe agent-check(format / lint / typecheck) cleanuv run poe test— 3235 passed, 2 skippeduv run poe validate-openapicleanuv run poe validate-examples— 0 failuresValidationErrorend-to-end test produces expected output via both raw-httpx +KatanaClientpaths🤖 Generated with Claude Code