diff --git a/python/client-contract/CHANGELOG.md b/python/client-contract/CHANGELOG.md index 5cf3edf..a48bc05 100644 --- a/python/client-contract/CHANGELOG.md +++ b/python/client-contract/CHANGELOG.md @@ -9,12 +9,13 @@ All notable changes to the `dfo-spec-kit-client-contract` Python package will be - Package scaffolding (uv, ruff, ty, pytest). - `served/openapi.json`, the composed dfo-apis app's OpenAPI document, and `served/SOURCE.json`, naming dfo-apis commit `035d29cd05fb31d7cb47cfb33197d4002f773df6`. - `strict-fields.json` and its schema: 29 members that stay required and non-nullable, each one a client reads, and the answer kinds, part statuses and operation statuses the clients branch on. -- `actions.json` and its schema: the caller's next step for each of the 101 active refusal codes on `problem_details` or `no_data_reason`, with the gap kind of each no_data reason. +- `actions.json` and its schema: the caller's next step for each of the 102 active refusal codes on `problem_details` or `no_data_reason`, with the gap kind of each no_data reason. - The status fallback for a refusal whose code has no row: 400 and 422 to `InvalidRequestError`, 401 to `AuthenticationError`, 403 to `PermissionDeniedError`, 404 and 410 to `NotFoundError`, 408 to `DfoTimeoutError`, 429 to `RateLimitError`, and any other 4xx or 5xx to `InvalidRequestError` or `ServerError`. - `scenario.schema.json`, the scenario record, and 118 scenarios: 31 recorded against the deployed dfo-apis dev app at `035d29cd0`, and 87 synthetic, each derived from a recorded one by a one-line edit, with 19 binary bodies totalling 1,124,502 bytes and records and bodies together 13,080,568 bytes. The scenarios cover the several-part choice entries and the assumptions each identity entry echoes, a picked reading whose assumption Search states in its answer, the place choice through a no_data part's options, the choice refusals, a part without variable_text, an unknown status without a restatement, a shared label, a one-part option whose data_query holds two parts, the separate and operations echo, an unsupported part's placeholder, the timed_out, unsupported and unknown gaps, an unknown answer kind, the `SearchWarning` rule, the stacked dataset's `part` column, the frame layout, a strict fetch's `DataWarning`, a spec fetch that rebinds one of two entity roles and keeps the other, one that drops a role bound to null, a spec whose destination is a named group fetched with a window or with its origin rebound, whose sentence lookup posts the groups of the roles the build left bound, a twice cut-off build, a cut-off build retried after its Retry-After, a build refused 504 then 503 and one refused 503 then 504, a refused build, a build refused 501 `capability_unavailable`, a digest mismatch, a signed URL refused on both rounds, a relative files-route Location, an artifact url on a foreign origin, on the storage host and absolute on the API's origin, a manifest definition without identity or schema_version, a Search spec without identity or resolved, nested specs no rule reads, an empty parts list, a Search body that is not an object, a codeless 503 problem document that is not retryable, a 401, an identity resubmit answering another spec, a part whose status is not a string, a Search spec whose definition names another spec_id, a Search response with no JSON body, an option with no data_query, empty entities and facets, a Sunset header issued once, `x-dfo-warning` messages and a Deprecation and Sunset pair, an unreadable versions page, a mint and a spec fetch whose build answers 202 and is polled to done, a resumed operation, a rate-limited poll, 503 polls retried within the budget an answered poll restores, a 404 poll that raises, a failed operation posted again, a failed operation with no error, and a ref whose stored file expired, read through the file selector, rebuilt at the same version, at a newer one, or refused with the newest version read from the versions route: the first version it lists with no `base_content_version`, the newest plain build, so a derived version listed before it is skipped. - `client-codes.json` and its schema: the 23 codes a client sets on the errors it raises itself, each with its error class, retryable flag and condition, disjoint from `actions.json` and `registries/codes.json`, and the `client_codes()` and `client_codes_schema()` accessors. Every scenario error the client raises itself carries its code, and `refusal_without_code` names a refusal whose body carries no code. - `proxy-codes.json` and its schema, the website proxy's `rate_limited` and `transient_unavailable` codes with their statuses, and `warning-codes.json` and its schema, the 6 codes a client sets on the warnings it issues itself, with the `proxy_codes()` and `warning_codes()` accessors. - `ref_digest_changed` in `warning-codes.json`, a `DfoWarning` for a ref member whose `content_version` serves a file with another digest than the ref's, because the server rendered the same data again. The client returns the data, with no error under strict. +- `timed_out` in `actions.json` and `registries/codes.json`, the no_data reason Search gives a part it left unresolved at the request's time limit, with action `ask_data_question` and gap kind `timed_out`, which `actions.schema.json` now admits. The client-side `timeout` code, a transport timeout, is unchanged. - Error outcomes give the error's `trace_id`, the body's, else the `x-dfo-trace-id` header's, else null. Scenario outcomes give `retry_waits`, the waits before each retry, and `warnings`, the warning codes a step's responses issued. Scenario steps take no `timeout`. - `dataset-ref.schema.json`, the frozen `DatasetRef` format, with accepted and rejected examples. - `without_party_totals` in the ref's artifact rendering, `true` for the version rendered with party totals left out and absent for a plain rendering, with `ref-without-party-totals` and `ref-without-party-totals-false-rejected` as its examples. diff --git a/python/client-contract/README.md b/python/client-contract/README.md index 3e323d6..601c45b 100644 --- a/python/client-contract/README.md +++ b/python/client-contract/README.md @@ -97,7 +97,7 @@ The required members are each part's `status` and its payload (`spec`, `candidat | `report` | `ServerError` | Reports the trace id, after the client's retries | | `upgrade` | `ContractError` | Upgrades the client | -A code that maps to `no_data_reason` also carries `gap_kind`, the kind of gap a no_data part with that reason becomes: `choice`, `no_data`, `not_understood` or `unsupported`. `choose` goes with `choice` and only with it. `choose` and `ask_data_question` name a next step inside an answer, so a code that also reaches a problem document takes another action. The website proxy's own `{error, retry, detail}` body carries its code in `error`. `proxy-codes.json` lists those codes with their status, `rate_limited` at 429 and `transient_unavailable` at 502, and the validator refuses a proxy body whose `error` it does not list at that status. No proxy code has a row here, so a client raises a proxy refusal with the class its status maps to. `capability_unavailable` is a well-formed request for a format, reshape or conversion class the deployment does not enable, answered 501; its action is `fix_request`, since the caller asks for another one, and `fetch-spec-capability-unavailable` pins it. +A code that maps to `no_data_reason` also carries `gap_kind`, the kind of gap a no_data part with that reason becomes: `choice`, `no_data`, `not_understood`, `unsupported` or `timed_out`. `timed_out` is a part Search left unresolved at its time limit; sending the question again, or a narrower one, may answer it. `choose` goes with `choice` and only with it. `choose` and `ask_data_question` name a next step inside an answer, so a code that also reaches a problem document takes another action. The website proxy's own `{error, retry, detail}` body carries its code in `error`. `proxy-codes.json` lists those codes with their status, `rate_limited` at 429 and `transient_unavailable` at 502, and the validator refuses a proxy body whose `error` it does not list at that status. No proxy code has a row here, so a client raises a proxy refusal with the class its status maps to. `capability_unavailable` is a well-formed request for a format, reshape or conversion class the deployment does not enable, answered 501; its action is `fix_request`, since the caller asks for another one, and `fetch-spec-capability-unavailable` pins it. A client maps a refusal whose code has no row, or that carries no code, on its HTTP status: diff --git a/python/client-contract/src/dfo_client_contract/actions.json b/python/client-contract/src/dfo_client_contract/actions.json index 7220529..34b2b09 100644 --- a/python/client-contract/src/dfo_client_contract/actions.json +++ b/python/client-contract/src/dfo_client_contract/actions.json @@ -388,6 +388,11 @@ "code": "spec_not_found", "action": "check_id" }, + { + "code": "timed_out", + "action": "ask_data_question", + "gap_kind": "timed_out" + }, { "code": "timeout", "action": "report" diff --git a/python/client-contract/src/dfo_client_contract/actions.schema.json b/python/client-contract/src/dfo_client_contract/actions.schema.json index 70c6fa2..a102481 100644 --- a/python/client-contract/src/dfo_client_contract/actions.schema.json +++ b/python/client-contract/src/dfo_client_contract/actions.schema.json @@ -48,7 +48,8 @@ "choice", "no_data", "not_understood", - "unsupported" + "unsupported", + "timed_out" ] } } diff --git a/python/spec-kit/src/dfo_spec_kit/registry/codes.json b/python/spec-kit/src/dfo_spec_kit/registry/codes.json index 976cd64..0a351ae 100644 --- a/python/spec-kit/src/dfo_spec_kit/registry/codes.json +++ b/python/spec-kit/src/dfo_spec_kit/registry/codes.json @@ -4662,6 +4662,26 @@ ], "lifecycle": "active" }, + { + "code": "timed_out", + "classification": "refusal", + "definition": "Search stopped at the request's time limit before it resolved this part; the same question, or a narrower one, may answer when sent again.", + "owning_contract": "search", + "owner": "platform", + "introduced_in": "v1", + "mappings": [ + { + "endpoint": "search.resolve", + "doors": [ + "raw_text", + "parsed", + "dataset_resubmit" + ], + "channel": "no_data_reason" + } + ], + "lifecycle": "active" + }, { "code": "timeout", "classification": "refusal", diff --git a/registries/codes.json b/registries/codes.json index 976cd64..0a351ae 100644 --- a/registries/codes.json +++ b/registries/codes.json @@ -4662,6 +4662,26 @@ ], "lifecycle": "active" }, + { + "code": "timed_out", + "classification": "refusal", + "definition": "Search stopped at the request's time limit before it resolved this part; the same question, or a narrower one, may answer when sent again.", + "owning_contract": "search", + "owner": "platform", + "introduced_in": "v1", + "mappings": [ + { + "endpoint": "search.resolve", + "doors": [ + "raw_text", + "parsed", + "dataset_resubmit" + ], + "channel": "no_data_reason" + } + ], + "lifecycle": "active" + }, { "code": "timeout", "classification": "refusal", diff --git a/tools/tests/test_validate_client_contract.py b/tools/tests/test_validate_client_contract.py index 7ce8bde..fd00276 100644 --- a/tools/tests/test_validate_client_contract.py +++ b/tools/tests/test_validate_client_contract.py @@ -511,6 +511,7 @@ def change(doc: Any) -> None: "is listed twice", ), ("actions.json", _drop_action("spec_not_found"), "'spec_not_found' has no row"), + ("actions.json", _drop_action("timed_out"), "'timed_out' has no row"), ( "actions.json", _add_action({"code": "alternates_unevaluated", "action": "report"}), @@ -1621,6 +1622,7 @@ def change(doc: Any) -> None: "strict-enum-value-gone", "strict-pointer-repeated", "action-row-missing", + "action-row-timed-out-missing", "action-row-for-a-warning", "action-row-for-an-unknown-code", "actions-unsorted",