Skip to content

docs(openapi)!: match the spec to live API behavior - #438

Merged
dodeja merged 20 commits into
mainfrom
openapi-fixes
Oct 17, 2026
Merged

dodeja merged 20 commits into
mainfrom
openapi-fixes

Conversation

@dodeja

@dodeja dodeja commented Oct 17, 2026 •

Copy link
Copy Markdown
Member

Why

#433 merged before its review and live-test fixes landed. We ran every SDK, MCP, and CLI operation against a local backend built from the latest staging (51711be). Many spec claims did not match what the API returns. This PR fixes the spec so it describes the API as it behaves today.

What changed

  • docs(openapi)!: document 204 deletes and shared error responses
  • docs(openapi)!: mark relationships and columns staging serves as null
  • docs(openapi)!: type port and metro area coordinates as decimal strings
  • docs(openapi): describe resource links and raw pagination links
  • docs(openapi)!: allow terminals, transport events, and raw events in container included
  • docs(openapi)!: document tracking request cancellation and ref_numbers edits
  • docs(openapi)!: add missing webhook events and default active to true
  • docs(openapi)!: document GET /search result type as search_result
  • docs(openapi)!: match the webhook notification examples response
  • docs(openapi)!: document account custom field definitions and entity types
  • docs(openapi)!: edit documents with document_type, not document_type_manual
  • docs(openapi)!: document 404 on every lookup by path parameter
  • docs(openapi): document the container edit attributes and errors
  • docs(openapi)!: allow null pod_terminal data on containers
  • docs(openapi): give every nullable schema a type
  • docs(openapi)!: document the tracking request user relationship and accepted includes
  • docs(openapi): document the user-provided MBL and booking numbers on shipment edits

Scope

This PR is spec-first. It also carries the SDK edits that keep each commit compiling against the regenerated terminal49.ts, plus the regenerated SDK reference pages. Each commit body names its edits.

Tradeoffs

The spec documents current behavior even where the backend is wrong. Backend fixes are tracked in Linear: DEV-13512, DEV-13513, DEV-13514, DEV-13515, DEV-13516, DEV-13632.

Blast Radius

Commits marked ! change generated SDK types: response unions, nullability, required fields, and route shapes. Code that relied on the old, inaccurate types may need updates. The Postman collection is left for the CI bot to regenerate.

Verification

  • vp run check and vp run test pass at every commit (git rebase -x).
  • generate:types, SDK docs, and MCP filter generation show no drift at the tip.
  • Live responses validated against this branch's docs/openapi.json.

Stack: this PR, then SDK fixes, then the MCP and CLI fixes on top of the SDK fixes.

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

RetriggerView in GreptileConfidence Score: 4/5

The PR appears safe to merge, with one non-blocking correction needed for null metro-area responses.

Fix All in CodexFindings

  1. P2 Null lookup responses remain invalid ▶
Fix with agent prompt
### Issue 1
docs/openapi.json:10156-10162
The schema for `GET /metro_areas/{id}` still rejects the documented `data: null` response for unknown IDs. The outer `nullable: true` allows null for its own `type`, but `allOf` also requires the value to satisfy `metro_area`, which only accepts an object. Engineers validating responses against the spec can therefore reject a normal lookup response. Use an endpoint-specific nullable schema that does not also require a non-null object.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

Updates the OpenAPI spec, generated TypeScript types, and reference pages to describe the live API.

  • Account custom fields now have their own routes and entity names.
  • Pending tracking requests can now be cancelled through a documented API route.
  • Webhook docs now list more events and match notification payloads.
  • SDK requests and API docs now use the observed field and response shapes.

Acknowledged by dodeja: the PR intentionally documents current backend defects rather than fixing them, including invalid enum writes, authorization responses, and cancellation of unknown IDs. Further SDK, MCP, and CLI fixes are deferred to later PRs.

Reviews (1) · Last reviewed commit: "docs(openapi): document the user-provide..." · Reviewed by Greptile

@mintlify

mintlify Bot commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated
terminal49 🟢 Ready View Preview Oct 17, 2026, 1:58 AM

@vercel

vercel Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
api Ready Ready Preview Oct 17, 2026 6:03am UTC

Request Review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 17, 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-10-17T06:06:10.021712Z b00a12f 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.

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3f76c08390

ℹ️ 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".

Comment thread docs/openapi.json
"$ref": "#/components/schemas/link"
},
{
"$ref": "#/components/schemas/shipment"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Allow null reference-number entries in included shipments

The newly supported shipment variant does not describe every payload already documented for this endpoint: the Transport Event response example contains attributes.ref_numbers: [null], while the referenced shipment schema permits only strings. Because the SDK response types are generated from this OpenAPI file, code can treat each entry as a string and fail when this live shape is returned; make the item schema nullable or normalize the response.

AGENTS.md reference: AGENTS.md:L51-L54

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b00a12f. shipments.ref_numbers is a Postgres text array, and the shipment update stores its entries without checking them, so a null entry can be served. The items schema is now nullable, the generated type is (string | null)[], and the Transport Event example validates.

Comment thread docs/openapi.json
"$ref": "#/components/schemas/shipment"
},
{
"$ref": "#/components/schemas/container"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Allow null shipment relationships on included containers

The newly supported container variant is too strict for the Container Updated Event example in this same endpoint, where relationships.shipment.data is null; the referenced container schema requires that value to be an object. Generated SDK consumers will therefore receive a non-null relationship type for a payload that can be null and may dereference it unsafely, so the relationship data needs to be nullable.

AGENTS.md reference: AGENTS.md:L51-L54

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in aa1b4e5. Cargo declares belongs_to :shipment, optional: true, so a container can be served with relationships.shipment.data: null. That data is now nullable, and the Container Updated Event example validates.

Comment thread docs/openapi.json Outdated
Comment on lines +10156 to +10162
"type": "object",
"allOf": [
{
"$ref": "#/components/schemas/metro_area"
}
],
"nullable": true

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Null lookup responses remain invalid

The schema for GET /metro_areas/{id} still rejects the documented data: null response for unknown IDs. The outer nullable: true allows null for its own type, but allOf also requires the value to satisfy metro_area, which only accepts an object. Engineers validating responses against the spec can therefore reject a normal lookup response. Use an endpoint-specific nullable schema that does not also require a non-null object.

Prompt To Fix With AI
This is a comment left during a code review.
Path: docs/openapi.json
Line: 10156-10162

Comment:
**Null lookup responses remain invalid**

The schema for `GET /metro_areas/{id}` still rejects the documented `data: null` response for unknown IDs. The outer `nullable: true` allows null for its own `type`, but `allOf` also requires the value to satisfy `metro_area`, which only accepts an object. Engineers validating responses against the spec can therefore reject a normal lookup response. Use an endpoint-specific nullable schema that does not also require a non-null object.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Codex Fix in Claude Code

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. data is now a oneOf of metro_area and a null-only schema. The response has two examples, one with a metro area and one for an unknown ID that returns data: null. Local staging returns data: null with status 200 for an unknown ID.

@dodeja
dodeja added this pull request to stack #442 October 17, 2026 05:08
dodeja and others added 19 commits October 16, 2026 22:11
Add shared BadRequest, Unauthorized, NotFound and UnprocessableEntity
responses and reference them where staging returns those statuses.
Deletes of webhooks, custom fields, definitions and options return 204.
The error object no longer requires title, because Pundit 401s carry
only detail. Types are regenerated.

BREAKING CHANGE: in the exported `paths` type from @terminal49/sdk,
DELETE /webhooks/{id}, /custom_fields/{id},
/custom_field_definitions/{id}, and
/custom_field_definitions/{definition_id}/options/{option_id} answer
204 instead of 200, so their `responses[200]` keys become
`responses[204]`. `components['schemas']['error'].title` becomes
optional.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Shipment pod_terminal, destination_terminal and
line_tracking_stopped_by_user are belongs_to links that serialize
{data: null}. Terminal address and code columns are nullable. Raw event
location and vessel can be null, and the container and terminal links
are now documented. Map GeoJSON port country, country_code and
time_zone copy nullable port columns. Types are regenerated.

BREAKING CHANGE: in the exported @terminal49/sdk types, relationship
data, terminal address and code, raw event location and vessel, and
map GeoJSON port country, country_code, and time_zone allow null.
`container_pod_terminal_changed_event` relationships `container`,
`shipment`, and `terminal` move `id` and `type` under a nullable
`data` object, and `type` narrows from string to
'container_pod_terminal_changed_event'. Read
`relationships.container.data?.id` instead of
`relationships.container.id`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ports.latitude and longitude are numeric(13,9) columns. MetroArea reads
the same table, and Rails serializes BigDecimal as a string, so both
resources return values like "22.57489971". Types are regenerated.

BREAKING CHANGE: port and metro area latitude and longitude in the
exported @terminal49/sdk types change from number to string, which
matches the decimal strings the API returns.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Per-resource links.self values, such as data.links.self on a shipment,
are paths like /v2/shipments/{id}, so they use uri-reference. The
top-level links.self on single-resource responses is the absolute
request URL with its query string, so it drops the uri format. On a
create it is the collection URL, not the new resource.

Pagination links come from jsonapi.rb. self repeats the request URL as
sent. The other links CGI-unescape the query and leave page[number]
brackets raw. None of them use the uri format. Pagination links also
include current.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…container included

ContainerSerializer exposes pod_terminal, pickup_facility,
transport_events, and raw_events, and the default include already
returns terminals. GET /containers `included` now allows shipment,
terminal, transport_event, and raw_event items. GET /containers/{id}
`included` gains raw_event, so both endpoints document the same item
types. Types are regenerated.

BREAKING CHANGE: in the exported `paths` type from @terminal49/sdk,
GET /containers `included` widens from `shipment[]` to
`(shipment | terminal | transport_event | raw_event)[]`, so code must
narrow on `type` before it reads shipment attributes. GET
/containers/{id} `included` gains `raw_event`, so an exhaustive
switch over its item `type` must handle raw events.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s edits

TrackingRequestStateMachine has a cancelled state that sets
failed_reason to "Cancelled by user", and the index filter accepts
status=cancelled. Document PATCH /tracking_requests/{id}/cancel, which
returns {} or a 422 whose errors value is a string. PATCH
/tracking_requests/{id} accepts only ref_numbers, which replaces the
list; ref_number was silently ignored. Types are regenerated.

BREAKING CHANGE: in the exported `paths` type from @terminal49/sdk,
PATCH /tracking_requests/{id} takes `ref_numbers: string[]` instead
of `ref_number`. Tracking request `status` adds 'cancelled', and
`failed_reason` adds 'Cancelled by user', 'awaiting_manifest', and
'shipping_line_not_supported'. Exhaustive switches over either field
must handle the new values.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
WebhookEventsValidator accepts container.detention_lfd_line.changed,
container.delivery_appointment.changed, shipment.routing.filled and
link.created, so add them to every webhook event enum and the event
catalog. POST /webhooks requires url and a non-empty events list (the
validator rejects an empty list with 422). active is optional because
the webhooks.active column defaults to true. Types are regenerated.

BREAKING CHANGE: in the exported `paths` type from @terminal49/sdk,
POST /webhooks requires `events` instead of `active`, which becomes
optional. Webhook `events`, webhook notification `event`, and the
other webhook event enums gain values, so exhaustive switches over them
must handle the new members.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SearchResultSerializer has no set_type, so every row is typed
search_result. The matched kind is attributes.entity_type, which is
shipment, cargo or tracking_request. Types are regenerated.

BREAKING CHANGE: in the exported `paths` type from @terminal49/sdk,
GET /search result `type` is 'search_result' instead of 'shipment' |
'cargo' | 'tracking_request', and `attributes.entity_type` is
'shipment' | 'cargo' | 'tracking_request' instead of a string. Read the
kind from `attributes.entity_type`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GET /webhook_notifications/examples renders one serializable_hash, so
data is a single webhook_notification and there are no links or meta.
included carries the shipment, container, port, terminal, vessel and
event records the postman INCLUDE_MAP names. event is required: a
missing or unavailable event returns 400. The example payload page no
longer shows a top-level links object, which delivered payloads lack.
Types are regenerated.

BREAKING CHANGE: in the exported `paths` type from @terminal49/sdk,
GET /webhook_notifications/examples requires `query.event` and
returns one `data: webhook_notification` with `included:
webhook_notification_included[]`, instead of a
`webhook_notification[]` list with `links` and `meta`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…types

For customer accounts the top-level /custom_field_definitions list
returns Terminal49 templates and create returns 401. Document GET and
POST /accounts/{account_id}/custom_field_definitions, which serve the
account's own definitions and accept template_id. Definitions accept
entity_type Shipment or Cargo, and the custom_fields filter matches
Shipment, Cargo or TrackingRequest exactly. POST /custom_fields accepts
a tracking_request entity. The per-entity custom_fields lists accept
include=definition,entity,updated_by. Options are not paginated.

Reference pages now describe the template routes, the 422 on deleting
a definition that has data, and the account-only api_slug lookup.

Types and SDK reference pages are regenerated. To keep the SDK compiling, customFields.list
types its entityType filter from the GET /custom_fields query, and the
two custom field list queries keep accepting page params until the SDK
stops sending them.

BREAKING CHANGE: in the exported `paths` type from @terminal49/sdk,
custom field definition `entity_type` is 'Shipment' | 'Cargo' in
requests and responses; 'Container' and 'TrackingRequest' are removed.
`filter[entity_type]` is an enum instead of a string: 'Shipment' |
'Cargo' on GET /custom_field_definitions, and 'Shipment' | 'Cargo' |
'TrackingRequest' on GET /custom_fields. GET /custom_fields and GET
/custom_field_definitions no longer accept `page[number]` or
`page[size]`. Their 200 bodies, and that of GET
/custom_field_definitions/{definition_id}/options, drop `meta`. PATCH
/custom_field_definitions/{id} attributes drop `validation` and
`default_value`, which the API ignores. In the SDK,
customFields.list and customFieldDefinitions.list no longer accept
'Container' as an entityType filter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…manual

DocumentsController#update_params permits only extracted_data_manual,
document_type and classification_notes, so document_type_manual was
dropped silently. Setting document_type marks document_type_source as
customer. The document schema now lists document_type_source, which
the serializer emits, instead of document_type_manual, which it never
did. Validation failures return 422.

Types are regenerated. documents.update sends its documentTypeManual
value as document_type, so the override now takes effect.

BREAKING CHANGE: in the exported `paths` type from @terminal49/sdk,
PATCH /documents/{id} takes `document_type` instead of
`document_type_manual`, and document responses replace
`document_type_manual` with `document_type_source`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ApiBaseController rescues ActiveRecord::RecordNotFound with a JSON:API
404 for every action, and staging returned that body for GET
/terminals/{id} and GET /webhooks/{id}, which documented no 404.
Reference the shared NotFound response from each operation that takes
a path parameter and lacked one. GET /metro_areas/{id} is the
exception: an unknown id returns 200 with null data. Its data is a
oneOf of metro_area and a null-only schema, because OpenAPI 3.0
`nullable` beside a $ref still requires the referenced object, and
examples show both responses. Types are regenerated.

BREAKING CHANGE: GET /metro_areas/{id} `data` in the exported
`paths` type from @terminal49/sdk may be null.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PATCH /containers/{id} accepts seal_number, equipment_type,
equipment_length, equipment_height, and weight_in_lbs as well as
ref_numbers (staging containers_controller.rb UPDATE_PARAMS). Document
them with the values the API accepts. equipment_type and
equipment_height are the cargo.rb container_type and container_height
enum keys, and equipment_length is one of EDITABLE_CONTAINER_LENGTHS.
None of the three accepts null, because cargo.rb requires all three
once any of them changes. Document the 401 from the policy check, the
404 from the lookup by ID, and the 422 for a changed number, a failed
validation, or an unknown enum value. Types and SDK reference pages are regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Staging's ContainerSerializer declares has_one :pod_terminal, and
cargos.pod_terminal_id is nullable, so a container with no POD
terminal yet is served with relationships.pod_terminal.data: null. The
live webhook notification examples run failed schema validation on
exactly that. The spec typed the data as an object only, while its
sibling pickup_facility already allowed null. Types are regenerated,
so the exported container type no longer claims the data is always
an object.

BREAKING CHANGE: a container's relationships.pod_terminal.data in the
exported @terminal49/sdk types becomes nullable, so code that reads
it must handle null.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
OpenAPI 3.0 ignores `nullable` on a schema without `type`, and Ajv
refuses to compile one ("nullable" cannot be used without "type"), so
Spectral's oas3-valid-media-example rule failed on six request
examples that reach custom_field_value. Move `nullable` onto the
string branch of custom_field_value. The regenerated types are
equivalent: custom_field_value still admits null.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ccepted includes

V2::TrackingRequestSerializer declares belongs_to :user, so tracking
requests carry a user relationship and include=user returns the user.
The spec now documents that relationship and the includes the
endpoints accept. Types are regenerated.

BREAKING CHANGE: in the exported `paths` type from @terminal49/sdk,
GET /tracking_requests and GET /tracking_requests/{id} `included` items
can be `user`, so an exhaustive switch over the item `type` must handle
users.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…shipment edits

V2::ShipmentsController#update accepts user_provided_mbl_number and
user_provided_booking_number alongside ref_numbers and tags, but the
PATCH /shipments/{id} request body listed only ref_numbers and tags.
The spec now documents both string attributes. Types are regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cargo declares belongs_to :shipment with optional: true, so a container
with no shipment is served with relationships.shipment.data: null. The
Container Updated Event example on GET /webhook_notifications/{id}
shows exactly that, and it failed validation against the included
container variant. Types are regenerated.

BREAKING CHANGE: a container's relationships.shipment.data in the
exported @terminal49/sdk types becomes nullable, so code that reads it
must handle null.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
shipments.ref_numbers is a Postgres text array, and the shipment
update deserializes ref_numbers without checking its entries, so a
stored entry can be null and is served as null. The Transport Event
example on GET /webhook_notifications/{id} includes a shipment with
ref_numbers: [null], and it failed validation against the included
shipment variant. Types are regenerated.

BREAKING CHANGE: a shipment's attributes.ref_numbers in the exported
@terminal49/sdk types becomes (string | null)[], so code that reads an
entry must handle null.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b00a12fd8e

ℹ️ 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".

Comment thread docs/openapi.json
Comment on lines +4902 to +4903
"description": "The webhook notification event name you wish to see an example of",
"required": true

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Require an event in the SDK wrapper

Once this parameter is required, the public SDK remains inconsistent with the documented API: webhookNotifications.examples(event?: string) still accepts no event and client.write-endpoints.test.ts explicitly exercises examples(), which sends an undefined query value and receives the newly documented 400 response. Make the wrapper parameter required and use the generated WebhookEvent type so SDK callers cannot issue this invalid request.

AGENTS.md reference: AGENTS.md:L51-L54

Useful? React with 👍 / 👎.

Comment thread docs/openapi.json
Comment on lines +15404 to +15406
"attributes": {
"type": "object",
"properties": {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Encode the required account-definition attribute alternatives

For the new account-scoped create route, the schema leaves every attribute optional even though the description requires either template_id or all four of entity_type, api_slug, display_name, and data_type; it also leaves the outer request body and data optional. Consequently, the generated post-account-custom-field-definitions type accepts requests such as an empty attributes object that the documented API always rejects with 422. Mark the body/data as required and represent the two valid attribute shapes with oneOf or anyOf.

AGENTS.md reference: AGENTS.md:L51-L54

Useful? React with 👍 / 👎.

@dodeja
dodeja merged commit 9059a19 into main Oct 17, 2026
8 checks passed
dodeja added a commit that referenced this pull request Oct 17, 2026
Main's spec now declares components.responses (#438), and the rebase's
text merge added the trade intelligence block as a second "responses"
key in the same object. JSON parsers keep the last key, but YAML-strict
tooling such as openapi-typescript rejects the duplicate. Both sets now
live in one block.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
dodeja added a commit that referenced this pull request Oct 17, 2026
Regenerated on top of main after #438 and #439.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
dodeja added a commit that referenced this pull request Oct 17, 2026
Regenerated on top of main after #438 and #439.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot mentioned this pull request Oct 17, 2026
dodeja added a commit that referenced this pull request Oct 17, 2026
Main's spec now declares components.responses (#438), and the rebase's
text merge added the trade intelligence block as a second "responses"
key in the same object. JSON parsers keep the last key, but YAML-strict
tooling such as openapi-typescript rejects the duplicate. Both sets now
live in one block.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
dodeja added a commit that referenced this pull request Oct 17, 2026
Regenerated on top of main after #438 and #439.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
dodeja added a commit that referenced this pull request Oct 18, 2026
…415)

* docs: add Trade Intelligence endpoints to the API reference

Documents the nine /v2/trade_intel endpoints (US import bill-of-lading
records from US Customs manifests, January 2022 onward) under a public
Trade Intelligence tag: request and response schemas as TradeIntel*
components, the 400/401/403/422/429/502/504 error bodies, and endpoint
descriptions that carry the data caveats (physical containers,
estimated values, partial current month, names split by state or
withheld, substring company filter, carrier spelling variants).

Adds the reference pages to the API Reference navigation and
regenerates the Postman collection.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(sdk): add tradeIntel namespace for trade intelligence

client.tradeIntel exposes meta, searchCompanies, companyProfile,
searchCommodities, topImporters, trends, breakdown, lookupContainer, and
lookupBillOfLading. They return the endpoints' plain JSON bodies with
types generated from docs/openapi.json, and surface errors through the
existing error types: the 403 "not enabled" body maps to
FeatureNotEnabledError, 400 and 422 (including FastAPI detail arrays)
to ValidationError.

The trade intelligence POSTs are read-only queries, so the retry policy
now treats POSTs under /trade_intel/ like GETs: 429, 5xx, and network
failures are retried with backoff. Other writes still need an
Idempotency-Key. Types are generated with --default-non-nullable=false
so request fields with server defaults (limit, months, top) stay
optional; no existing type changes.

Fixture-based tests cover every method (path, method, body, response
passthrough) and the 400/422/403/429/502 paths. Regenerates the SDK
reference docs and documents the namespace.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs: state the real commodity ordering in trade intelligence lookups

Lookups sort commodity lines by bill, then by estimated value within each
bill, and the 200-line cap applies to that order. The schemas promised a
global highest-value-first order, which a multi-bill response does not
give. Found in Codex review.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs: mark always-returned trade intelligence response fields required

meta, the companies/search results, commodities/search results, trend
points, and top importers always carry these fields in production, so
the generated SDK types no longer make them optional. Fields that are
conditional stay optional: a trend point's partial flag, a ranking's
estimated_value without hs4, and a profile's details when found is
false. Raised in Codex review on the PR.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* chore(sdk): regenerate types and reference for required response fields

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs: restrict trade intelligence month fields to real calendar months

since and until in trends and breakdown accepted any two-digit month,
so schema-validating clients and generated Postman examples allowed
values like 2026-60. Raised in Codex review on the PR.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(mcp): trade intelligence tools (#417)

* feat(mcp): add trade intelligence tools

Seven read-only tools for US ocean import market data, built on the
SDK's tradeIntel namespace: search_importers, get_importer_profile,
search_commodities, rank_importers, get_trade_trends,
get_trade_breakdown, and get_trade_data_coverage. Record-level container
and bill of lading lookups stay in the SDK only.

The tool descriptions and server instructions carry the caveats agents
got wrong without guidance: search ranks by match not size and splits
companies by state, the profile with months: 24 is the way to get one
company's trend (the trends company filter is a substring match),
trends and breakdowns include the incomplete current month, history
starts in January 2022, missing volume can mean withheld names, values
are estimates, and volumes don't add across products or companies.

Inputs are strict (enums for measure, interval, dimensions, and coast;
real YYYY-MM months; bounded limits). A 403 returns a plain
"not enabled, contact support@terminal49.com" result instead of an
error; 400 shows the API's reason, and 429 and 5xx get plain messages
without internals. Responses drop internal fields, round numbers, cap
long lists, and flag truncation.

Adds unit and server tests, eval cases that pass with or without the
feature, the new tools in both store listings, and an Unreleased
changelog entry.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs(mcp): document the trade intelligence tools

Adds a trade intelligence section to the MCP tools reference with each
tool's parameters, example questions, the per-account enablement note,
and the caveats for reading the numbers.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(mcp): address Codex review of the trade intelligence tools

- A high notify-party share is a clue, not proof of a forwarder: large
  cargo owners can be mostly notify party, so the descriptions and
  instructions now say to weigh the name and product mix too.
- Name and filter inputs accept up to 200 characters, matching the API,
  so a long company name from search can be reused in later calls.
- search_importers reports more_not_shown when it cuts a row's ports,
  origins, carriers, or products to five, and search_commodities keeps
  up to 15 common goods and counts the rest in common_goods_not_shown.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs(mcp): qualify the notify-party signal and the common goods cap

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test(mcp): skip trade intelligence evals where the tools aren't deployed

Push-time CI runs the live eval against production, which doesn't list
tools a PR adds until it deploys. The new cases now skip when the server
under test doesn't list the tool, and still run against the PR preview.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(mcp): describe the search window honestly and score gated evals fairly

- search_importers results cover the search index's 12-month window,
  which can end a month or two before the last full month. The period
  note, min_containers, and the tool description now say so and point to
  get_importer_profile for the latest months.
- Eval cases for account-gated tools score a "not enabled" reply as the
  allowed outcome it is, so the scorecard and report no longer count it
  as a contract failure. Adds unit tests for that scorer.

Raised in the Codex and Greptile reviews on the PR.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs(mcp): spell out TEU and HS, and describe the search window

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(mcp): cap trend series by whole periods and breakdowns by whole levels

Trend points arrive ordered by period then group, so cutting the flat
series at 500 dropped the newest periods and could split one, while the
reported range still claimed them. The series now keeps the most recent
whole periods, reports the range actually returned, and counts the
earlier periods left out. Breakdowns drop whole deeper levels instead of
cutting a parent's children. Raised in Codex review on the PR.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(mcp): describe default windows and min_containers precisely

The trends and breakdown defaults run from about two years (or one
year) back through the current, incomplete month, inclusive, so they are
not a clean 24 or 12 months. Descriptions now say so and tell agents to
set since and until for annual totals. min_containers applies to the
search window in period, not the last 12 full months. Raised in Codex
review on the PR.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs(mcp): describe the trends and breakdown default windows precisely

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(mcp): keep a capped trend's range in one period format

A capped quarterly or yearly series reported a range mixing a point
label (2026-Q1) with a month boundary (2026-06). period now always holds
the requested month range, and periods_shown gives the first and last
periods actually returned, in the series' own labels. Raised in Codex
review on the PR.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* chore: regenerate SDK types, SDK reference docs, and Postman collection

Regenerated once on top of main after the rebase (main's SDK resync in

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs: state which trade intelligence dimensions can't be combined

Lane dimensions (pol, pol_country, container_type) can't be combined with
company or product dimensions in one request, and estimated_value isn't
available by them; the API answers 400. The MCP tool descriptions said
so but the API reference didn't. Raised in Codex review on the PR.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs: merge trade intelligence responses into the shared responses block

Main's spec now declares components.responses (#438), and the rebase's
text merge added the trade intelligence block as a second "responses"
key in the same object. JSON parsers keep the last key, but YAML-strict
tooling such as openapi-typescript rejects the duplicate. Both sets now
live in one block.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(sdk): let tradeIntel calls infer their types from the spec

Transport.execute now infers the response body type from the typed
openapi-fetch call (#439), so the explicit body type arguments no longer
fit its signature. The inferred types are the same TradeIntel* schemas.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* chore: regenerate SDK types, SDK reference docs, and Postman collection

Regenerated on top of main after #438 and #439.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs: require exact-length trade intelligence state, SCAC and HS4 codes

State filters (company_state, dest_state, state) now require two letters,
the scac filter four letters, and top importers hs4 four digits, matching
the MCP tool schemas. maxLength alone let values like "F" or "123" through.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(sdk): keep the RetryInterceptor doc comment on the class

The retry explanation sat above the new ReplayableRequest and
RetryRequestParams declarations, so the generated reference lost it.
Move it back above RetryInterceptor and say that read-only trade
intelligence query POSTs are retried like GETs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore: regenerate SDK reference docs and Postman collection

Regenerated after the trade intelligence code patterns and the
RetryInterceptor doc comment move. SDK types and MCP filter schemas
are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore: Auto-generate Postman collection from openapi.json [skip ci]

* docs(mcp): say estimated_value is not available by lane

The API rejects estimated_value with pol, pol_country or container_type
as a dimension or filter (docs/openapi.json TradeIntelDimension and
TradeIntelFilters). The MCP guide presented estimated_value as generally
available, so it now states the restriction for get_trade_trends and
get_trade_breakdown and lists the lane combination rules with the other
trade intelligence caveats.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(mcp): warn that estimated_value is not available by lane

The measure and dimension descriptions for get_trade_trends and
get_trade_breakdown now say that estimated_value cannot be used with
pol, pol_country or container_type, which the API answers with a 400.
A protocol test checks the advertised measure description and the MCP
docs section for both tools.

Addresses the Codex review on packages/mcp/src/tools/trade-intel.ts:82.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(mcp): count all 21 tools in the README, overview and setup guide

The PR registers seven trade intelligence tools, but the MCP README
header and table, the TOOLS_OVERVIEW summary and the setup guide's
"list the tools" check still said 14. The README table now lists the
trade intelligence tools too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(mcp): keep documented tool counts and the README table on tools/list

Fails when the README, TOOLS_OVERVIEW or the setup guide state a tool
count other than the tools/list length, or when the README tool table
misses or adds a tool.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(mcp): require the lane-filter case in the estimated_value warning

The API rejects estimated_value when pol, pol_country or container_type
is used at all, as a dimension or as a filter. The test now fails if the
tool description or the docs drop the filter case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(mcp): pin the trade trends and breakdown default windows

Without since, the trade-intel service returns the N months ending with
until (24 for trends, 12 for breakdown), so since is N - 1 months before
until. The test fails while tools/list, docs/mcp/home.mdx or the OpenAPI
spec describe the default as "N months ago" or "about a year back".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(trade-intel): state the trends and breakdown default windows exactly

Without since, the API returns the N months ending with until (24 for
trends, 12 for breakdown), so since defaults to N - 1 months before
until. The since fields said "Defaults to 24/12 months ago", which reads
as N + 1 calendar months. The breakdown and trends descriptions and the
MCP guide now give the same window. Source: analytics._range in the
trade-intel service.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore: Auto-generate Postman collection from openapi.json [skip ci]

* fix(mcp,sdk): describe the trade default windows as the API computes them

get_trade_trends and get_trade_breakdown said "about two years back" and
"about a year back, not a clean 12 months". They now say the window is
the 24 or 12 months ending with the current, incomplete month, with
since 23 or 11 months before until. The SDK JSDoc, regenerated types
and SDK reference match the OpenAPI source.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(openapi): match the trade intelligence contract to what the service returns

Checked against Terminal49/t49 staging (apps/trade-intel build.py,
analytics.py, api.py, index.py):

- meta dates (since_month, until_month_exclusive, facts_since_month,
  partial_month, refreshed_since) and company first_month/last_month are
  first-of-month dates, YYYY-MM-DD; facts_until_exclusive is the day after
  the build; mode is full or incremental.
- importers/top: states can be null, estimated_value is always sent and is
  null without hs4, and filters echo port_of_discharge as pod.
- companies/profile: until is absent when found is false, and totals.states
  can be null.
- trends periods are always YYYY-MM (the first month of the quarter or
  year), not YYYY-Qn or YYYY.
- a 422 can also be the query cost-cap error, { error }.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(sdk): type trade intelligence nulls and keep retries within maxRetries

Regenerated types from the corrected spec, so TradeIntelTopImporter
states and estimated_value, profile totals.states and profile until are
typed as the service sends them, ranking filters expose pod, and the 422
body includes the cost-cap { error } variant. Type tests in
spec-types.test.ts fail against the old types.

Fixtures now use the service's real formats (YYYY-MM-DD meta dates,
YYYY-MM quarterly periods), and the live smoke test expects
facts_since_month as a first-of-month date.

RetryInterceptor kept separate retry counters in onError and onResponse,
so network failure, network failure, 503, 503, success made five requests
with maxRetries 2. Both paths now draw on one per-request budget.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore: Auto-generate Postman collection from openapi.json [skip ci]

* fix(mcp): read the trade index dates the service actually sends

The service sends meta and search-window dates as YYYY-MM-DD, but
previousMonth() only accepted YYYY-MM, so get_trade_data_coverage lost
last_full_month and search_window.to, and search_importers lost
period.to. Index dates are now accepted in either form and shown as
YYYY-MM, including first_month and last_month. rank_importers also leaves
out estimated_value_usd when the service sends null instead of emitting
an undefined key.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

This branch was successfully deployed

2 active (1 outdated) deployments
Preview — a725a851 Deployed Oct 17, 2026 by vercel[bot]
staging - docs — 3f76c083 Deployed Oct 17, 2026 by mintlify[bot]
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.

1 participant