diff --git a/python/client-contract/CHANGELOG.md b/python/client-contract/CHANGELOG.md index cfd2adc..aa12883 100644 --- a/python/client-contract/CHANGELOG.md +++ b/python/client-contract/CHANGELOG.md @@ -35,9 +35,11 @@ All notable changes to the `dfo-spec-kit-client-contract` Python package will be - A derived member's root records its rendering in `root_rendering`, the artifact selector's members and `groupby`, the sum DFO read the root with as `options.groupby` states it: `by`, one or more distinct column names, `op` `sum`, and `levels`, a depth from 0 to 99 for each kept hierarchical axis, absent when none is set. A breakdown or a sum over StatVars reads a root that way. The artifact's `rendering` admits no `groupby`. `ref-derived-grouped-root` is accepted, its `by` sorted as DFO records it and its identities computed by the dfo-apis analysis runner over the scenario's rows summed to their purpose's sector code, and `ref-derived-root-groupby-op-rejected` and `ref-artifact-groupby-rejected` are rejected. The validator checks a summed root's rendering against its scenario's build without its `groupby`, which no recorded build posts. - The analysis and publication rules DFO applies, each with vectors and a reference implementation in `tests/test_analysis_rules.py`: `output-digest`, the `out2` digest of a derived frame's values with the `stat_var`, members, period and unit each belongs to, without `measurementMethod`, `provenance`, `status` or a unit's `recipe`, reading the variable `compare`, `sum_of`, `share_of` or `quotient_of` writes under an `mv_` name hashed from its verb and its inputs' variables, 35 characters at any depth, so an engine release or upstream rebuild that moves no number keeps the digest, with 22 canonicalisation vectors, each minted name given with its name document and one naming a `quotient_of` over a `sum_of` by the inner step's name; `output-comparison`, how `==` and `same_data` compare two refs, with 12 vectors; `operation-outputs`, `share_of` in per cent under `Percent`, `join` in long format by `stat_var`, `compare` as data minus other, `sum_of` as data plus other, and `growth` as a fraction in `value` with no unit, leaving out a member with no value at an end of its window, a relative change over a zero or sign-crossing base and a change that is not finite, and refusing two rows of one member in one period with `role_ambiguous`, with 10 vectors; `rolling-composition`, one latest period per recipe tree, refusing rolling steps over different populations with `rolling_populations_differ` even when they settle on one year, with 6 vectors; and `publication`, a share's numbered slots that keep their URLs across a live replace, a snapshot that refuses `share_not_replaceable`, and the write token checks, with 8 vectors. - `actions.json` rows for `allowance_exhausted` (`wait_for_reset`), `rolling_populations_differ`, `several_members`, `share_not_replaceable` and `write_token_required` (`fix_request`), and `write_token_invalid` (`stop`). -- The `stop` action, class `PermissionDeniedError`, for a refusal no permission or credential the caller can obtain clears: `write_token_invalid`, since a share takes only the write token issued when it was created and a share created without one takes none. +- The `stop` action, class `PermissionDeniedError`, for a refusal no permission or credential the caller can obtain clears: `write_token_invalid`, since a share takes only the write token issued when it was created and a share created without one takes none, `licence_not_redistributable`, since a source's licence forbids the export for every caller, and `provenance_unavailable`, since a recipe root that cannot be cited survives a rebuild. - The retry rules: a refusal whose code's action is `wait_for_reset` is never retried and raises `QuotaExceededError` at once, whatever its document and status say, and a refusal whose `Retry-After` is longer than 60 seconds (`RETRY_AFTER_MAX_S`) raises at once with that wait, a refused poll included. `search-retry-after-past-cap` pins the cap. - `series_left_out` in `warning-codes.json`, class `SeriesLeftOutWarning`, and the `series-left-out` rule with 8 vectors: a fetch of derived members issues it once when any member's derived manifest lists left-out series in `left_out` or says `left_out_complete` false, a null `left_out` issues nothing, and a strict fetch raises it in place of its data. `tools/validate_client_contract.py` takes the code from the rule. - `actions.json` rows (`fix_request`) for the 34 refusal codes the analysis engine serves on `datasets.analysis`, including `compare_no_key_matched` and `sum_of_no_key_matched`. +- `actions.json` rows (`stop`) for the two refusals of the exports door, `provenance_unavailable` and `licence_not_redistributable`. +- `tools/validate_client_contract.py` lets a proxy code share a registry code whose every mapping is `mcp_refusal`, as `rate_limited` and `transient_unavailable` do. [unreleased]: https://github.com/DevelopmentFinanceObservatory/spec-kit/commits/main/python diff --git a/python/client-contract/README.md b/python/client-contract/README.md index 0a0aa87..9f475e8 100644 --- a/python/client-contract/README.md +++ b/python/client-contract/README.md @@ -98,7 +98,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`, `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 answers its own refusals with problem documents of the same shape, with `type`, `title`, `status`, `detail`, `code`, `retryable` and `trace_id`, the trace id repeated in the `x-dfo-trace-id` header. `proxy-codes.json` lists their codes with their status, `rate_limited` at 429, which also sends `Retry-After`, and `transient_unavailable` at 502, both retryable. The validator refuses a problem document whose code is neither an active registry code nor a proxy code at that status. No proxy code has a row here, so a client raises a proxy refusal with the class its status maps to: `RateLimitError` for `rate_limited`, which `search-proxy-rate-limited` pins, and `ServerError` for `transient_unavailable`, which `search-proxy-unavailable` pins. `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. `write_token_invalid` takes `stop`: a share takes only the write token issued when it was created, and a share created without one takes none, so no scope or new token lets the caller replace or delete it. `allowance_exhausted` takes `wait_for_reset`, and a client raises it at once as `QuotaExceededError` with its `Retry-After`, the time until the allowance resets, as the retry rules below state. +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 answers its own refusals with problem documents of the same shape, with `type`, `title`, `status`, `detail`, `code`, `retryable` and `trace_id`, the trace id repeated in the `x-dfo-trace-id` header. `proxy-codes.json` lists their codes with their status, `rate_limited` at 429, which also sends `Retry-After`, and `transient_unavailable` at 502, both retryable. Both are also registered on `mcp.tools`, which reaches no problem document, so the proxy's use of them takes no row. The validator refuses a problem document whose code is neither an active registry code nor a proxy code at that status. No proxy code has a row here, so a client raises a proxy refusal with the class its status maps to: `RateLimitError` for `rate_limited`, which `search-proxy-rate-limited` pins, and `ServerError` for `transient_unavailable`, which `search-proxy-unavailable` pins. `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. `write_token_invalid` takes `stop`: a share takes only the write token issued when it was created, and a share created without one takes none, so no scope or new token lets the caller replace or delete it. `licence_not_redistributable` takes `stop` too: a source's licence forbids the export for every caller, so no scope or credential serves the file. So does `provenance_unavailable`: a recipe root that cannot be cited survives a rebuild, so nothing the caller can change names the frame's sources. `allowance_exhausted` takes `wait_for_reset`, and a client raises it at once as `QuotaExceededError` with its `Retry-After`, the time until the allowance resets, as the retry rules below state. 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 66a24f5..96a3121 100644 --- a/python/client-contract/src/dfo_client_contract/actions.json +++ b/python/client-contract/src/dfo_client_contract/actions.json @@ -224,6 +224,10 @@ "code": "join_output_collision", "action": "fix_request" }, + { + "code": "licence_not_redistributable", + "action": "stop" + }, { "code": "member_not_resolvable", "action": "fix_request" @@ -330,6 +334,10 @@ "code": "population_empty", "action": "fix_request" }, + { + "code": "provenance_unavailable", + "action": "stop" + }, { "code": "provider_ambiguous", "action": "fix_request" diff --git a/python/spec-kit/CHANGELOG.md b/python/spec-kit/CHANGELOG.md index 58f5965..6c5e2eb 100644 --- a/python/spec-kit/CHANGELOG.md +++ b/python/spec-kit/CHANGELOG.md @@ -260,6 +260,68 @@ and this package adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 mirror: a 422 refusal on `search.resolve` for a `dataset_resubmit` whose identity names an open role, when the StatVars the definition elects declare no role on that axis, so the definition cannot give one series per member. +- Codes `tool_not_found`, `frame_ref_incomplete`, `frame_not_held`, + `frame_expired`, `rolling_reanchor_unavailable`, `provenance_unavailable` + and `licence_not_redistributable` in `registries/codes.json` and its bundled + mirror, all refusals. The first five are refusals only an MCP tool result + carries, on endpoint `mcp.tools` and channel `mcp_refusal`. They cover an + unknown or removed tool, a frame reference that lacks its content version, a + frame not held, a frame aged out, and a rolling computation with a derived + input that has no stored recipe to compute again under the shared latest + period. `provenance_unavailable` (422) and `licence_not_redistributable` + (403) are on `datasets.exports`, the latter also on `datasets.export_link`. + They cover an export of a frame whose sources and licences cannot be named, + and an export whose source licence forbids redistribution. +- Codes `definition_not_held`, `derived_input_unrecoverable`, + `entity_not_found`, `frame_integrity_failed`, `operation_still_processing`, + `publish_not_configured`, `rate_limited` and `transient_unavailable` in + `registries/codes.json` and its bundled + mirror, all refusals on `mcp.tools` and `mcp_refusal`. They cover an id with + no registered definition, an input frame whose period labels do not state + their years, a scope name that gives no entity, a stored frame that fails an + integrity check, a build unfinished when the call's wait ended, a deployment + with no identity to publish a frame under, a service at a limit on calls, + and a service behind the one called that failed or a lost frame not yet + computable. The website proxy's `proxy-codes.json` holds the proxy surface + of `rate_limited` and `transient_unavailable`. +- `mcp.tools` mappings on the 54 existing codes the MCP servers also write, so + the endpoint holds every code they write: each value of the data-mcp and + analysis-mcp `ToolErrorCode` enums and each code presentation-mcp and the + harness combined server write. Platform codes presentation-mcp only forwards + stay on their `presentation.*` mappings. +- Endpoint `mcp.tools` and channel `mcp_refusal` in + `schemas/registry/code-registry.schema.json`, its README and + `tools/validate_registry.py`. They describe the refusal envelope of an MCP + tool result (dfo-ai data-mcp, analysis-mcp, presentation-mcp). A mapping on + the endpoint carries no `http_status`, `mcp_refusal` is a refusal-only + channel, and the schema requires each of the two for the other. +- Endpoints `datasets.exports` and `datasets.export_link` in + `schemas/registry/code-registry.schema.json` and its README. + `datasets.exports` is `POST /api/datasets/exports`, which prepares an export + file of a stored frame. It carries `request_invalid` (400), `spec_not_found` + (404), `snapshot_not_found` (404), `snapshot_revoked` (410), + `artifact_expired` (410), `identity_quarantined` (500), `over_ceiling` + (422), `provenance_unavailable` (422), `licence_not_redistributable` (403), + `allowance_exhausted` (429), `service_unavailable` (503), `internal_error` + (500), `capability_unavailable` (501), `excel_limit` (422), + `excel_illegal_character` (422), `parquet_size_bound` (422) and + `rollup_not_summable` (422). A prepare that names only a `spec_id` + resolves its newest version, which can rebuild it, so the endpoint also + carries the codes a rebuild raises on `datasets.definition`, at the same + statuses: `spec_not_exportable` (409), `identity_collision` and + `members_not_held` (500), `upstream_error` (502), + `upstream_version_changed` (503), `timeout` (504), and + `collection_probe_ceiling`, `column_unknown`, `default_facet_undefined`, + `definition_not_reproducible`, `excluded_by_registry`, + `facet_binding_unsupported`, `facet_not_available`, `invalid_role_ref`, + `member_not_resolvable`, `rate_coverage_gap`, `recipe_not_declared`, + `role_incompatible` and + `role_signature_incompatible` (422). `datasets.export_link` is + `GET /api/datasets/exports/{export_id}`, which answers a 302 redirect to the + file's download link. It carries `not_found` (404), + `snapshot_revoked` (410), `licence_not_redistributable` (403), + `identity_quarantined` (500), `service_unavailable` (503) and + `internal_error` (500). - Codes `requested_option_unavailable` and `conflicting_requested_options` in `registries/codes.json` and its bundled mirror: 422 refusals on `search.resolve` for a `dataset_resubmit` that posts `scope_entries`. 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 9e0ae18..b35f039 100644 --- a/python/spec-kit/src/dfo_spec_kit/registry/codes.json +++ b/python/spec-kit/src/dfo_spec_kit/registry/codes.json @@ -23,6 +23,15 @@ "endpoint": "presentation.shares", "channel": "problem_details", "http_status": 429 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 429 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -78,6 +87,11 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 410 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 410 } ], "lifecycle": "active" @@ -329,6 +343,15 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 501 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 501 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -475,6 +498,11 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 } ], "lifecycle": "active" @@ -517,6 +545,11 @@ "endpoint": "datasets.definition", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 } ], "lifecycle": "active" @@ -549,6 +582,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -565,6 +602,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -729,6 +770,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -745,6 +790,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -761,6 +810,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -777,6 +830,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -859,6 +916,15 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -878,6 +944,21 @@ ], "lifecycle": "active" }, + { + "code": "definition_not_held", + "classification": "refusal", + "definition": "No definition is registered under the id a request names, and the service that holds definitions is up. A new search finds an id that is registered.", + "owning_contract": "mcp", + "owner": "platform", + "introduced_in": "v1", + "mappings": [ + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" + } + ], + "lifecycle": "active" + }, { "code": "definition_not_reproducible", "classification": "refusal", @@ -915,6 +996,11 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 } ], "lifecycle": "active" @@ -1052,6 +1138,25 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" + } + ], + "lifecycle": "active" + }, + { + "code": "derived_input_unrecoverable", + "classification": "refusal", + "definition": "An input frame has period labels that do not state the years they cover, so no verb can read its periods. Only running the analysis again from the source data gives a usable frame.", + "owning_contract": "mcp", + "owner": "platform", + "introduced_in": "v1", + "mappings": [ + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -1068,6 +1173,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -1287,6 +1396,10 @@ "dataset_resubmit" ], "channel": "no_data_reason" + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -1311,6 +1424,21 @@ ], "lifecycle": "active" }, + { + "code": "entity_not_found", + "classification": "refusal", + "definition": "A name in a request's scope gives no entity to fetch data for: it matches nothing, the graph holds no node for it, or it names a group with no member countries.", + "owning_contract": "mcp", + "owner": "platform", + "introduced_in": "v1", + "mappings": [ + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" + } + ], + "lifecycle": "active" + }, { "code": "entity_not_resolved", "classification": "refusal", @@ -1438,6 +1566,11 @@ "endpoint": "datasets.files", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 } ], "lifecycle": "active" @@ -1464,6 +1597,11 @@ "endpoint": "datasets.files", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 } ], "lifecycle": "active" @@ -1500,6 +1638,11 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 } ], "lifecycle": "active" @@ -1560,6 +1703,11 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 } ], "lifecycle": "active" @@ -1603,6 +1751,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -1668,6 +1820,15 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -1720,6 +1881,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -1736,6 +1901,55 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" + } + ], + "lifecycle": "active" + }, + { + "code": "frame_expired", + "classification": "refusal", + "definition": "The frame a reference names was held and has aged out, and the server can still tell that it was.", + "owning_contract": "mcp", + "owner": "platform", + "introduced_in": "v1", + "mappings": [ + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" + } + ], + "lifecycle": "active" + }, + { + "code": "frame_integrity_failed", + "classification": "refusal", + "definition": "A stored frame failed an integrity check, so it does not match what was recorded when it was written. The same reference reaches the same frame, so only a fresh result under a new reference clears it.", + "owning_contract": "mcp", + "owner": "platform", + "introduced_in": "v1", + "mappings": [ + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" + } + ], + "lifecycle": "active" + }, + { + "code": "frame_not_held", + "classification": "refusal", + "definition": "The frame a reference names is not held: it was never stored, it is no longer held and nothing records that it was, or no such version exists. The same code answers a reference whose identity and version do not belong together.", + "owning_contract": "mcp", + "owner": "platform", + "introduced_in": "v1", + "mappings": [ + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -1756,6 +1970,21 @@ ], "lifecycle": "active" }, + { + "code": "frame_ref_incomplete", + "classification": "refusal", + "definition": "A frame reference carries a derived-frame id without its content version, or an id that is not a dataset id, so the server cannot fetch the frame by itself.", + "owning_contract": "mcp", + "owner": "platform", + "introduced_in": "v1", + "mappings": [ + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" + } + ], + "lifecycle": "active" + }, { "code": "frame_too_large", "classification": "refusal", @@ -1768,6 +1997,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -1794,6 +2027,10 @@ "endpoint": "presentation.shares", "channel": "problem_details", "http_status": 404 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -1810,6 +2047,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -1856,6 +2097,11 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 500 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 500 } ], "lifecycle": "active" @@ -1927,6 +2173,16 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 500 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 500 + }, + { + "endpoint": "datasets.export_link", + "channel": "problem_details", + "http_status": 500 } ], "lifecycle": "active" @@ -2018,6 +2274,20 @@ "endpoint": "presentation.shares", "channel": "problem_details", "http_status": 500 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 500 + }, + { + "endpoint": "datasets.export_link", + "channel": "problem_details", + "http_status": 500 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -2060,6 +2330,11 @@ "endpoint": "datasets.options", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 } ], "lifecycle": "active" @@ -2076,6 +2351,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -2092,6 +2371,35 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" + } + ], + "lifecycle": "active" + }, + { + "code": "licence_not_redistributable", + "classification": "refusal", + "definition": "The licence of a source behind the requested frame forbids redistribution, so no export file is served.", + "owning_contract": "datasets", + "owner": "platform", + "introduced_in": "v1", + "mappings": [ + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 403 + }, + { + "endpoint": "datasets.export_link", + "channel": "problem_details", + "http_status": 403 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -2133,6 +2441,15 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -2206,6 +2523,11 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 500 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 500 } ], "lifecycle": "active" @@ -2277,6 +2599,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -2441,6 +2767,10 @@ "endpoint": "graph.documentation", "channel": "problem_details", "http_status": 404 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -2457,6 +2787,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -2488,6 +2822,11 @@ "endpoint": "presentation.shares", "channel": "problem_details", "http_status": 404 + }, + { + "endpoint": "datasets.export_link", + "channel": "problem_details", + "http_status": 404 } ], "lifecycle": "active" @@ -2857,6 +3196,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -2876,6 +3219,10 @@ ], "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -2912,6 +3259,21 @@ ], "lifecycle": "active" }, + { + "code": "operation_still_processing", + "classification": "refusal", + "definition": "A long-running build did not give its result before the call's wait ended. The build continues, and repeating the call later collects it.", + "owning_contract": "mcp", + "owner": "platform", + "introduced_in": "v1", + "mappings": [ + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" + } + ], + "lifecycle": "active" + }, { "code": "operation_uncovered", "classification": "refusal", @@ -2924,6 +3286,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -3058,6 +3424,15 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -3089,6 +3464,11 @@ "endpoint": "datasets.files", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 } ], "lifecycle": "active" @@ -3258,6 +3638,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -3347,6 +3731,26 @@ ], "lifecycle": "active" }, + { + "code": "provenance_unavailable", + "classification": "refusal", + "definition": "The sources of the requested frame and their licences cannot be named, so no export file is served.", + "owning_contract": "datasets", + "owner": "platform", + "introduced_in": "v1", + "mappings": [ + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" + } + ], + "lifecycle": "active" + }, { "code": "provider_ambiguous", "classification": "refusal", @@ -3387,6 +3791,21 @@ ], "lifecycle": "active" }, + { + "code": "publish_not_configured", + "classification": "refusal", + "definition": "This deployment has no identity to publish a frame under, so it cannot publish a frame to the platform.", + "owning_contract": "mcp", + "owner": "platform", + "introduced_in": "v1", + "mappings": [ + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" + } + ], + "lifecycle": "active" + }, { "code": "publish_not_permitted", "classification": "refusal", @@ -3477,6 +3896,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -3493,6 +3916,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -3509,6 +3936,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -3561,6 +3992,26 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 + } + ], + "lifecycle": "active" + }, + { + "code": "rate_limited", + "classification": "refusal", + "definition": "A limit on the calls a service accepts has been reached, so it refused this one. The identical call is accepted after the wait the refusal names.", + "owning_contract": "mcp", + "owner": "platform", + "introduced_in": "v1", + "mappings": [ + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -3621,6 +4072,11 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 } ], "lifecycle": "active" @@ -3745,6 +4201,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -3889,6 +4349,15 @@ "endpoint": "presentation.shares", "channel": "problem_details", "http_status": 400 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 400 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -4030,6 +4499,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -4100,6 +4573,11 @@ "endpoint": "datasets.options", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 } ], "lifecycle": "active" @@ -4205,6 +4683,11 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 } ], "lifecycle": "active" @@ -4239,6 +4722,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -4255,6 +4742,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -4275,6 +4766,21 @@ ], "lifecycle": "active" }, + { + "code": "rolling_reanchor_unavailable", + "classification": "refusal", + "definition": "A rolling computation reads every input under one latest period, and no recipe is stored for a derived input, so that input cannot be computed again under that period.", + "owning_contract": "mcp", + "owner": "platform", + "introduced_in": "v1", + "mappings": [ + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" + } + ], + "lifecycle": "active" + }, { "code": "rollup_not_summable", "classification": "refusal", @@ -4302,6 +4808,15 @@ "endpoint": "datasets.files", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -4515,6 +5030,20 @@ "endpoint": "presentation.shares", "channel": "problem_details", "http_status": 503 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 503 + }, + { + "endpoint": "datasets.export_link", + "channel": "problem_details", + "http_status": 503 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -4614,6 +5143,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -4630,6 +5163,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -4646,6 +5183,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -4662,6 +5203,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -4819,6 +5364,10 @@ "endpoint": "skills.resource", "channel": "problem_details", "http_status": 404 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -4892,6 +5441,10 @@ "endpoint": "skills.resource", "channel": "problem_details", "http_status": 404 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -5092,6 +5645,11 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 404 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 404 } ], "lifecycle": "active" @@ -5158,6 +5716,16 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 410 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 410 + }, + { + "endpoint": "datasets.export_link", + "channel": "problem_details", + "http_status": 410 } ], "lifecycle": "active" @@ -5330,6 +5898,15 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 409 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 409 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -5396,6 +5973,11 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 404 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 404 } ], "lifecycle": "active" @@ -5451,6 +6033,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -5625,6 +6211,41 @@ "endpoint": "presentation.shares", "channel": "problem_details", "http_status": 504 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 504 + } + ], + "lifecycle": "active" + }, + { + "code": "tool_not_found", + "classification": "refusal", + "definition": "The request names a tool the server does not offer, either because the name is unknown or because the tool was removed from the public list in favour of another.", + "owning_contract": "mcp", + "owner": "platform", + "introduced_in": "v1", + "mappings": [ + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" + } + ], + "lifecycle": "active" + }, + { + "code": "transient_unavailable", + "classification": "refusal", + "definition": "A service behind the one called could not be reached or failed, or a lost frame cannot be computed again yet, so the call has no answer now. The identical call is expected to succeed after a delay.", + "owning_contract": "mcp", + "owner": "platform", + "introduced_in": "v1", + "mappings": [ + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -5687,6 +6308,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -5901,6 +6526,15 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 502 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 502 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -5947,6 +6581,15 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 503 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 503 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" diff --git a/registries/codes.json b/registries/codes.json index 9e0ae18..b35f039 100644 --- a/registries/codes.json +++ b/registries/codes.json @@ -23,6 +23,15 @@ "endpoint": "presentation.shares", "channel": "problem_details", "http_status": 429 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 429 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -78,6 +87,11 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 410 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 410 } ], "lifecycle": "active" @@ -329,6 +343,15 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 501 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 501 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -475,6 +498,11 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 } ], "lifecycle": "active" @@ -517,6 +545,11 @@ "endpoint": "datasets.definition", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 } ], "lifecycle": "active" @@ -549,6 +582,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -565,6 +602,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -729,6 +770,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -745,6 +790,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -761,6 +810,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -777,6 +830,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -859,6 +916,15 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -878,6 +944,21 @@ ], "lifecycle": "active" }, + { + "code": "definition_not_held", + "classification": "refusal", + "definition": "No definition is registered under the id a request names, and the service that holds definitions is up. A new search finds an id that is registered.", + "owning_contract": "mcp", + "owner": "platform", + "introduced_in": "v1", + "mappings": [ + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" + } + ], + "lifecycle": "active" + }, { "code": "definition_not_reproducible", "classification": "refusal", @@ -915,6 +996,11 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 } ], "lifecycle": "active" @@ -1052,6 +1138,25 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" + } + ], + "lifecycle": "active" + }, + { + "code": "derived_input_unrecoverable", + "classification": "refusal", + "definition": "An input frame has period labels that do not state the years they cover, so no verb can read its periods. Only running the analysis again from the source data gives a usable frame.", + "owning_contract": "mcp", + "owner": "platform", + "introduced_in": "v1", + "mappings": [ + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -1068,6 +1173,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -1287,6 +1396,10 @@ "dataset_resubmit" ], "channel": "no_data_reason" + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -1311,6 +1424,21 @@ ], "lifecycle": "active" }, + { + "code": "entity_not_found", + "classification": "refusal", + "definition": "A name in a request's scope gives no entity to fetch data for: it matches nothing, the graph holds no node for it, or it names a group with no member countries.", + "owning_contract": "mcp", + "owner": "platform", + "introduced_in": "v1", + "mappings": [ + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" + } + ], + "lifecycle": "active" + }, { "code": "entity_not_resolved", "classification": "refusal", @@ -1438,6 +1566,11 @@ "endpoint": "datasets.files", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 } ], "lifecycle": "active" @@ -1464,6 +1597,11 @@ "endpoint": "datasets.files", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 } ], "lifecycle": "active" @@ -1500,6 +1638,11 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 } ], "lifecycle": "active" @@ -1560,6 +1703,11 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 } ], "lifecycle": "active" @@ -1603,6 +1751,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -1668,6 +1820,15 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -1720,6 +1881,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -1736,6 +1901,55 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" + } + ], + "lifecycle": "active" + }, + { + "code": "frame_expired", + "classification": "refusal", + "definition": "The frame a reference names was held and has aged out, and the server can still tell that it was.", + "owning_contract": "mcp", + "owner": "platform", + "introduced_in": "v1", + "mappings": [ + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" + } + ], + "lifecycle": "active" + }, + { + "code": "frame_integrity_failed", + "classification": "refusal", + "definition": "A stored frame failed an integrity check, so it does not match what was recorded when it was written. The same reference reaches the same frame, so only a fresh result under a new reference clears it.", + "owning_contract": "mcp", + "owner": "platform", + "introduced_in": "v1", + "mappings": [ + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" + } + ], + "lifecycle": "active" + }, + { + "code": "frame_not_held", + "classification": "refusal", + "definition": "The frame a reference names is not held: it was never stored, it is no longer held and nothing records that it was, or no such version exists. The same code answers a reference whose identity and version do not belong together.", + "owning_contract": "mcp", + "owner": "platform", + "introduced_in": "v1", + "mappings": [ + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -1756,6 +1970,21 @@ ], "lifecycle": "active" }, + { + "code": "frame_ref_incomplete", + "classification": "refusal", + "definition": "A frame reference carries a derived-frame id without its content version, or an id that is not a dataset id, so the server cannot fetch the frame by itself.", + "owning_contract": "mcp", + "owner": "platform", + "introduced_in": "v1", + "mappings": [ + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" + } + ], + "lifecycle": "active" + }, { "code": "frame_too_large", "classification": "refusal", @@ -1768,6 +1997,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -1794,6 +2027,10 @@ "endpoint": "presentation.shares", "channel": "problem_details", "http_status": 404 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -1810,6 +2047,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -1856,6 +2097,11 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 500 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 500 } ], "lifecycle": "active" @@ -1927,6 +2173,16 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 500 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 500 + }, + { + "endpoint": "datasets.export_link", + "channel": "problem_details", + "http_status": 500 } ], "lifecycle": "active" @@ -2018,6 +2274,20 @@ "endpoint": "presentation.shares", "channel": "problem_details", "http_status": 500 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 500 + }, + { + "endpoint": "datasets.export_link", + "channel": "problem_details", + "http_status": 500 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -2060,6 +2330,11 @@ "endpoint": "datasets.options", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 } ], "lifecycle": "active" @@ -2076,6 +2351,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -2092,6 +2371,35 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" + } + ], + "lifecycle": "active" + }, + { + "code": "licence_not_redistributable", + "classification": "refusal", + "definition": "The licence of a source behind the requested frame forbids redistribution, so no export file is served.", + "owning_contract": "datasets", + "owner": "platform", + "introduced_in": "v1", + "mappings": [ + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 403 + }, + { + "endpoint": "datasets.export_link", + "channel": "problem_details", + "http_status": 403 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -2133,6 +2441,15 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -2206,6 +2523,11 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 500 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 500 } ], "lifecycle": "active" @@ -2277,6 +2599,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -2441,6 +2767,10 @@ "endpoint": "graph.documentation", "channel": "problem_details", "http_status": 404 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -2457,6 +2787,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -2488,6 +2822,11 @@ "endpoint": "presentation.shares", "channel": "problem_details", "http_status": 404 + }, + { + "endpoint": "datasets.export_link", + "channel": "problem_details", + "http_status": 404 } ], "lifecycle": "active" @@ -2857,6 +3196,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -2876,6 +3219,10 @@ ], "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -2912,6 +3259,21 @@ ], "lifecycle": "active" }, + { + "code": "operation_still_processing", + "classification": "refusal", + "definition": "A long-running build did not give its result before the call's wait ended. The build continues, and repeating the call later collects it.", + "owning_contract": "mcp", + "owner": "platform", + "introduced_in": "v1", + "mappings": [ + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" + } + ], + "lifecycle": "active" + }, { "code": "operation_uncovered", "classification": "refusal", @@ -2924,6 +3286,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -3058,6 +3424,15 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -3089,6 +3464,11 @@ "endpoint": "datasets.files", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 } ], "lifecycle": "active" @@ -3258,6 +3638,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -3347,6 +3731,26 @@ ], "lifecycle": "active" }, + { + "code": "provenance_unavailable", + "classification": "refusal", + "definition": "The sources of the requested frame and their licences cannot be named, so no export file is served.", + "owning_contract": "datasets", + "owner": "platform", + "introduced_in": "v1", + "mappings": [ + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" + } + ], + "lifecycle": "active" + }, { "code": "provider_ambiguous", "classification": "refusal", @@ -3387,6 +3791,21 @@ ], "lifecycle": "active" }, + { + "code": "publish_not_configured", + "classification": "refusal", + "definition": "This deployment has no identity to publish a frame under, so it cannot publish a frame to the platform.", + "owning_contract": "mcp", + "owner": "platform", + "introduced_in": "v1", + "mappings": [ + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" + } + ], + "lifecycle": "active" + }, { "code": "publish_not_permitted", "classification": "refusal", @@ -3477,6 +3896,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -3493,6 +3916,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -3509,6 +3936,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -3561,6 +3992,26 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 + } + ], + "lifecycle": "active" + }, + { + "code": "rate_limited", + "classification": "refusal", + "definition": "A limit on the calls a service accepts has been reached, so it refused this one. The identical call is accepted after the wait the refusal names.", + "owning_contract": "mcp", + "owner": "platform", + "introduced_in": "v1", + "mappings": [ + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -3621,6 +4072,11 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 } ], "lifecycle": "active" @@ -3745,6 +4201,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -3889,6 +4349,15 @@ "endpoint": "presentation.shares", "channel": "problem_details", "http_status": 400 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 400 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -4030,6 +4499,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -4100,6 +4573,11 @@ "endpoint": "datasets.options", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 } ], "lifecycle": "active" @@ -4205,6 +4683,11 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 } ], "lifecycle": "active" @@ -4239,6 +4722,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -4255,6 +4742,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -4275,6 +4766,21 @@ ], "lifecycle": "active" }, + { + "code": "rolling_reanchor_unavailable", + "classification": "refusal", + "definition": "A rolling computation reads every input under one latest period, and no recipe is stored for a derived input, so that input cannot be computed again under that period.", + "owning_contract": "mcp", + "owner": "platform", + "introduced_in": "v1", + "mappings": [ + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" + } + ], + "lifecycle": "active" + }, { "code": "rollup_not_summable", "classification": "refusal", @@ -4302,6 +4808,15 @@ "endpoint": "datasets.files", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -4515,6 +5030,20 @@ "endpoint": "presentation.shares", "channel": "problem_details", "http_status": 503 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 503 + }, + { + "endpoint": "datasets.export_link", + "channel": "problem_details", + "http_status": 503 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -4614,6 +5143,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -4630,6 +5163,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -4646,6 +5183,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -4662,6 +5203,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -4819,6 +5364,10 @@ "endpoint": "skills.resource", "channel": "problem_details", "http_status": 404 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -4892,6 +5441,10 @@ "endpoint": "skills.resource", "channel": "problem_details", "http_status": 404 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -5092,6 +5645,11 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 404 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 404 } ], "lifecycle": "active" @@ -5158,6 +5716,16 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 410 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 410 + }, + { + "endpoint": "datasets.export_link", + "channel": "problem_details", + "http_status": 410 } ], "lifecycle": "active" @@ -5330,6 +5898,15 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 409 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 409 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -5396,6 +5973,11 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 404 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 404 } ], "lifecycle": "active" @@ -5451,6 +6033,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -5625,6 +6211,41 @@ "endpoint": "presentation.shares", "channel": "problem_details", "http_status": 504 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 504 + } + ], + "lifecycle": "active" + }, + { + "code": "tool_not_found", + "classification": "refusal", + "definition": "The request names a tool the server does not offer, either because the name is unknown or because the tool was removed from the public list in favour of another.", + "owning_contract": "mcp", + "owner": "platform", + "introduced_in": "v1", + "mappings": [ + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" + } + ], + "lifecycle": "active" + }, + { + "code": "transient_unavailable", + "classification": "refusal", + "definition": "A service behind the one called could not be reached or failed, or a lost frame cannot be computed again yet, so the call has no answer now. The identical call is expected to succeed after a delay.", + "owning_contract": "mcp", + "owner": "platform", + "introduced_in": "v1", + "mappings": [ + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -5687,6 +6308,10 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 422 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -5901,6 +6526,15 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 502 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 502 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" @@ -5947,6 +6581,15 @@ "endpoint": "datasets.analysis", "channel": "problem_details", "http_status": 503 + }, + { + "endpoint": "datasets.exports", + "channel": "problem_details", + "http_status": 503 + }, + { + "endpoint": "mcp.tools", + "channel": "mcp_refusal" } ], "lifecycle": "active" diff --git a/schemas/registry/README.md b/schemas/registry/README.md index df4da75..ec64821 100644 --- a/schemas/registry/README.md +++ b/schemas/registry/README.md @@ -2,7 +2,7 @@ The code registry assigns one permanent, language-neutral meaning to every warning and refusal code, so that every consumer of a contract, the engine, the harness, a website, a partner integration, reads the same meaning for the same code. -The registry holds wire codes from Search, Presentation, Datasets, Graph, and Skills, plus the platform-generic codes any contract can render. The platform's `GET /codes/{code}` lookup, mounted on every surface, serves only codes this registry carries. `schemas/registry/code-registry.schema.json` (draft 2020-12, `$id` `https://raw.githubusercontent.com/DevelopmentFinanceObservatory/spec-kit/main/schemas/registry/code-registry.schema.json`) validates `registries/codes.json`. The `$id` sits under the repo's raw-content path because the kit has no schema-hosting service. Should one exist later, the `$id` moves and the old one is retired like any other schema identifier. +The registry holds wire codes from Search, Presentation, Datasets, Graph, Skills, and MCP tool results, plus the platform-generic codes any contract can render. The platform's `GET /codes/{code}` lookup, mounted on every surface, serves only codes this registry carries. `schemas/registry/code-registry.schema.json` (draft 2020-12, `$id` `https://raw.githubusercontent.com/DevelopmentFinanceObservatory/spec-kit/main/schemas/registry/code-registry.schema.json`) validates `registries/codes.json`. The `$id` sits under the repo's raw-content path because the kit has no schema-hosting service. Should one exist later, the `$id` moves and the old one is retired like any other schema identifier. `registries/codes.json` is canonical. `python/spec-kit/src/dfo_spec_kit/registry/codes.json` is a byte-identical package mirror because `uv_build` only includes files within a package directory. `dfo_spec_kit.registry` loads the mirror through `CodeRegistry`, `CodeEntry`, and `CodeMapping`. `uv run tools/validate_registry.py` fails if the files diverge. @@ -36,10 +36,10 @@ Each entry: `classification` collapses transport into two buckets: -- **`refusal`** covers a request that cannot be answered. It appears in a non-2xx RFC 9457 problem-details body, a 200 response's `no_data` reason, a 200 response's `unsupported` reason, a 200 `no_data` response's `flagged_match.gate`, the quality gate that failed and left a flagged nearest-real match in place of a confident answer, or a returned Spec's `observations.omitted.reason`, the 200 response's own explanation for why that Spec's observations preview carries no series. +- **`refusal`** covers a request that cannot be answered. It appears in an MCP tool result's refusal envelope, in a non-2xx RFC 9457 problem-details body, a 200 response's `no_data` reason, a 200 response's `unsupported` reason, a 200 `no_data` response's `flagged_match.gate`, the quality gate that failed and left a flagged nearest-real match in place of a confident answer, or a returned Spec's `observations.omitted.reason`, the 200 response's own explanation for why that Spec's observations preview carries no series. - **`warning`** covers a diagnostic attached to an otherwise-answered request. It appears in `diagnostics.warnings`, an SSE stream's terminal `done` event, a Datasets manifest's `warnings` array, a Datasets options envelope's `warnings` array, or a Skills API list or skill body's `warnings` array. -A code can appear on more than one surface. For example, `facet_not_available` is a 422 problem-details refusal through a structured door and a `no_data` reason for a text query. It has one registry entry with multiple mappings. A mapping records every permitted surface for that meaning. `(endpoint, channel)` is unique within an entry's `mappings`. A code that varies by door on one endpoint and channel uses one mapping with every permitted door. +A code can appear on more than one surface. For example, `facet_not_available` is a 422 problem-details refusal through a structured door and a `no_data` reason for a text query. It has one registry entry with multiple mappings. A mapping records every permitted surface for that meaning, with two exceptions, both set out under `mcp.tools` below: a platform code presentation-mcp forwards, and the website proxy's surface. `(endpoint, channel)` is unique within an entry's `mappings`. A code that varies by door on one endpoint and channel uses one mapping with every permitted door. `observations_omission_reason` is `tools/validate_registry.py`'s one channel legal under both classifications, because three `observations.omitted.reason` codes are warning-classified: `observations_preview_too_large`, `observations_windowed_scope_unsupported` and `observations_converted_scope_unsupported`. The first is also minted as a `diagnostics_warning`/`done_event_warning`. The other two also ride `diagnostics_warning`. Every other `observations.omitted.reason` code is `refusal`-classified and reaches this channel alone, never `diagnostics.warnings`, since nothing else on this list is ever minted as a Warning. @@ -59,7 +59,7 @@ Retiring a door is a schema change, reviewed as one: dropping a value from `code - **`endpoint`** lists the Search endpoints `search.resolve` (`POST /api/search/resolve`) and `search.options` (`POST /api/search/options`). - The Datasets endpoints are `datasets.register` (`POST /api/datasets/register`), `datasets.mint` (`POST /api/datasets`), `datasets.definition` (`GET /api/datasets/{id}`), `datasets.options` (`GET /api/datasets/{id}/options`, the write-free preview door), `datasets.members` (`POST /api/datasets/{id}/members`, the write-free member read), `datasets.snapshot` (`GET /api/datasets/{id}/{content_version}`, and `GET /api/datasets/{id}/versions`, which pages a dataset's servable versions), `datasets.files` (`GET /api/datasets/{id}/{content_version}/files/{digest}`, which answers a stored artifact with a 302 redirect to its download link, and `GET /api/datasets/{id}/{content_version}/file`, which selects a stored version's file by format, bundle and layout, renders it when it is missing, and answers the same redirect), `datasets.description` (`GET /api/datasets/{id}/{content_version}/description`, a stored version's description, for a `spec_` id with a `cv_` version or an `ops_` id with an `ocv_` version, and `GET .../description/values`, which pages one dimension's values), `datasets.observations` (`GET /api/datasets/{id}/{content_version}/observations`, a stored version's observations), `datasets.pin` (`POST /api/datasets/{id}/{content_version}/pin`), `datasets.publish` (`POST /api/datasets/frames/publish`, the door an analysis service uses to publish an already-computed frame under its own identity pair), `datasets.recipe` (`PUT` and `GET /api/datasets/frames/{spec_id}/{content_version}/recipe`, the recorded computation behind a published frame, and `GET /api/datasets/recipes/replays/{replay_key}`, the frame stored under a replay key), `datasets.refresh` (`POST /api/datasets/frames/refresh`, which rebuilds a dataset frame's rendering on current data), `datasets.operations` (`GET /api/datasets/operations/{operation_id}`), and `datasets.analysis` (`POST /api/datasets/analysis`, which runs an analysis recipe tree and answers its derived frame, and `POST /api/datasets/analysis/anchor`, which resolves a recipe's latest period and stores nothing). + The Datasets endpoints are `datasets.register` (`POST /api/datasets/register`), `datasets.mint` (`POST /api/datasets`), `datasets.definition` (`GET /api/datasets/{id}`), `datasets.options` (`GET /api/datasets/{id}/options`, the write-free preview door), `datasets.members` (`POST /api/datasets/{id}/members`, the write-free member read), `datasets.snapshot` (`GET /api/datasets/{id}/{content_version}`, and `GET /api/datasets/{id}/versions`, which pages a dataset's servable versions), `datasets.files` (`GET /api/datasets/{id}/{content_version}/files/{digest}`, which answers a stored artifact with a 302 redirect to its download link, and `GET /api/datasets/{id}/{content_version}/file`, which selects a stored version's file by format, bundle and layout, renders it when it is missing, and answers the same redirect), `datasets.description` (`GET /api/datasets/{id}/{content_version}/description`, a stored version's description, for a `spec_` id with a `cv_` version or an `ops_` id with an `ocv_` version, and `GET .../description/values`, which pages one dimension's values), `datasets.observations` (`GET /api/datasets/{id}/{content_version}/observations`, a stored version's observations), `datasets.pin` (`POST /api/datasets/{id}/{content_version}/pin`), `datasets.publish` (`POST /api/datasets/frames/publish`, the door an analysis service uses to publish an already-computed frame under its own identity pair), `datasets.recipe` (`PUT` and `GET /api/datasets/frames/{spec_id}/{content_version}/recipe`, the recorded computation behind a published frame, and `GET /api/datasets/recipes/replays/{replay_key}`, the frame stored under a replay key), `datasets.refresh` (`POST /api/datasets/frames/refresh`, which rebuilds a dataset frame's rendering on current data), `datasets.operations` (`GET /api/datasets/operations/{operation_id}`), `datasets.analysis` (`POST /api/datasets/analysis`, which runs an analysis recipe tree and answers its derived frame, and `POST /api/datasets/analysis/anchor`, which resolves a recipe's latest period and stores nothing), `datasets.exports` (`POST /api/datasets/exports`, which prepares an export file of a stored frame and answers its facts), and `datasets.export_link` (`GET /api/datasets/exports/{export_id}`, which answers a 302 redirect to the export file's download link). The Presentation endpoints are `presentation.write` (`POST /api/presentation/components`, `PATCH /api/presentation/components/{component_id}`) `presentation.render` (`POST /api/presentation/components/{component_id}/revisions/{revision}/render` and `GET .../render/data`, which draw a stored revision on a named target frame and mint no revision), and `presentation.shares` (the routes under `/api/presentation/shares` that create, replace and delete a share). Search's and Datasets' `schema`, `capabilities`, and `healthz` routes and Presentation's read routes carry no permanent codes. @@ -67,13 +67,17 @@ Retiring a door is a schema change, reviewed as one: dropping a value from `code The Skills endpoints are `skills.list` (`GET /api/skills`), `skills.fetch` (`GET /api/skills/{skill_id}`), and `skills.resource` (`GET /api/skills/{skill_id}/resources/{path}`). All three carry `service_unavailable`, which is how they report an index that has not finished its first build. `skills.fetch` and `skills.resource` carry `skill_not_found` and `request_invalid`, and `skills.resource` alone carries `skill_resource_not_found`. Every skills warning code appears on `skills.list`, and each one describing a single skill also appears on `skills.fetch`. Skills' `capabilities` route carries no permanent codes. + The MCP endpoint is `mcp.tools`, which holds every code an MCP server writes in a tool result: the values of the dfo-ai data-mcp and analysis-mcp `ToolErrorCode` enums, the refusals presentation-mcp writes, and the refusals the harness combined server writes, among them `tool_not_found` and `rate_limited`. `mcp.tools` and the `mcp_refusal` channel go only with each other, which the schema enforces, so these mappings carry no `http_status`. A code an HTTP route also serves keeps its HTTP mappings beside its `mcp.tools` mapping, as `request_invalid` and `internal_error` do, and no HTTP route returns a code that maps here alone. presentation-mcp also forwards platform codes it does not write itself, such as `edit_conflict`, in the same envelope, and those keep their `presentation.*` mappings with no `mcp.tools` mapping. + + A website proxy code may share its spelling with an entry that maps to `mcp.tools` alone, as `rate_limited` and `transient_unavailable` do. The entry's meaning holds on both surfaces, and `python/client-contract/src/dfo_client_contract/proxy-codes.json` holds the proxy surface and its statuses. + The platform endpoints are `platform.any`, for requests no mounted surface classifies, and `platform.codes` (`GET /codes/{code}`). `platform.any` covers unmatched routes, disallowed methods, stalled deadlines, and uncaught exceptions. A code may map to a platform endpoint and a contract endpoint. - **`doors`** names the ways a request reaches the engine. Omit it when a code applies to every door. The values are `raw_text`, `parsed`, `dataset_resubmit`, and `options`. `raw_text` and `parsed` are Search's text doors. `dataset_resubmit` re-resolves parts of a prior response: its identity and candidates entries share the same promote machinery a resolved identity already carries (no extraction, no re-binding), and its `parsed` entries share the parsed door's own per-part fan-out (still no extraction, but binding does run). A code raised only inside extraction — the one phase every `dataset_resubmit` entry kind skips — carries only the text doors, so conjunction handling, near-neighbour checks, purpose broadening, and constraint-filter fallback are scoped there. A code raised inside binding, materialisation, the promote machinery, or the shared observations fetch carries `dataset_resubmit` alongside the text doors. `options` names the single door of its own endpoint. `doors` and `endpoint` are coupled. The schema enforces the relationship. `search.resolve` accepts only `raw_text`, `parsed`, and `dataset_resubmit`. A `search.options` mapping with `doors` must use exactly `["options"]`. A code on two endpoints needs one mapping for each endpoint. - Datasets, Presentation, Graph, Skills, and platform mappings omit `doors`. Each endpoint value names one route or platform-wide concept. + Datasets, Presentation, Graph, Skills, MCP, and platform mappings omit `doors`. Each endpoint value names one route or platform-wide concept. - **`channel`** identifies where the code appears. @@ -86,12 +90,13 @@ Retiring a door is a schema change, reviewed as one: dropping a value from `code - `flagged_match_gate` is the 200 `no_data` status's `gate`, the failed quality-gate code that produced a flagged nearest-real match in place of a confident answer. - `skills_warning` is an entry in a Skills API list body's or skill body's `warnings` array. - `observations_omission_reason` is a returned Spec's `observations.omitted.reason`, the 200 response's own explanation for why that Spec's observations preview carries no series. The Datasets observations body carries the same `omitted` object. + - `mcp_refusal` is the refusal envelope in an MCP tool result's structured content, made of `code`, `retry_mode`, `message` and `fix`. `retry_after_s` names the wait when the refusal has one. `upstream_code` names the dfo-apis code behind the refusal, on data-mcp and analysis-mcp, and `capability_version` the capability version the upstream answered with, on data-mcp alone. data-mcp sends those three as null when it has no value, and the other servers leave them out. The envelope has no HTTP status. `http_status` is present only when `channel` is `problem_details`. ## Severity and human-facing messages -The registry has no severity or message template. Each occurrence supplies them through the contract's `Warning` shape (`{code, severity, message}`) or problem-details body (`{code, title, detail, ...}`). The registry defines a code's meaning and permitted locations. +The registry has no severity or message template. Each occurrence supplies them through the contract's `Warning` shape (`{code, severity, message}`), problem-details body (`{code, title, detail, ...}`), or the `message` in an `mcp_refusal` envelope, which carries no severity. The registry defines a code's meaning and permitted locations. ## Adding an entry diff --git a/schemas/registry/code-registry.schema.json b/schemas/registry/code-registry.schema.json index 23765d4..22217f2 100644 --- a/schemas/registry/code-registry.schema.json +++ b/schemas/registry/code-registry.schema.json @@ -43,7 +43,7 @@ "pattern": "^[a-z][a-z0-9]*(_[a-z0-9]+)*$" }, "classification": { - "description": "refusal is a request that cannot be answered. It appears in a non-2xx problem-details body, a 200 no_data reason, a 200 unsupported reason, or a 200 no_data response's flagged_match.gate. warning is a diagnostic attached to an otherwise-answered request. It appears in diagnostics.warnings, the SSE stream's terminal done event, a Datasets manifest's warnings array, a Datasets options envelope's warnings array, or a Skills API list or skill body's warnings array.", + "description": "refusal is a request that cannot be answered. It appears in an MCP tool result's refusal envelope, in a non-2xx problem-details body, a 200 no_data reason, a 200 unsupported reason, or a 200 no_data response's flagged_match.gate. warning is a diagnostic attached to an otherwise-answered request. It appears in diagnostics.warnings, the SSE stream's terminal done event, a Datasets manifest's warnings array, a Datasets options envelope's warnings array, or a Skills API list or skill body's warnings array.", "enum": [ "refusal", "warning" @@ -70,7 +70,7 @@ "minLength": 1 }, "mappings": { - "description": "Every endpoint/door/channel combination this code is allowed to appear on.", + "description": "Every endpoint/door/channel combination this code is allowed to appear on. An MCP tool result may also carry a platform code presentation-mcp forwards, and the website proxy's surface is in proxy-codes.json; see schemas/registry/README.md.", "type": "array", "minItems": 1, "items": { @@ -117,6 +117,8 @@ "datasets.refresh", "datasets.operations", "datasets.analysis", + "datasets.exports", + "datasets.export_link", "graph.node", "graph.nodes", "graph.members", @@ -128,7 +130,8 @@ "platform.codes", "skills.list", "skills.fetch", - "skills.resource" + "skills.resource", + "mcp.tools" ] }, "doors": { @@ -146,7 +149,7 @@ } }, "channel": { - "description": "problem_details is a non-2xx RFC 9457 body. no_data_reason is the 200 no_data status's `reason`. unsupported_code is the 200 unsupported status's `code`. diagnostics_warning is an entry in the response envelope's diagnostics.warnings. done_event_warning is an entry in the SSE stream's terminal done event, for trailing work that finishes after the response event has shipped. manifest_warning is an entry in a Datasets manifest's warnings array or a Datasets options envelope's warnings array. flagged_match_gate is the failed quality-gate code in a 200 no_data response's `flagged_match.gate`, naming why the nearest real match was flagged in place of a confident answer. skills_warning is an entry in a Skills API list body's or skill body's warnings array. observations_omission_reason is a returned Spec's `observations.omitted.reason`, the 200 response's own explanation for why that Spec's observations preview carries no series. The Datasets observations body carries the same `omitted` object.", + "description": "problem_details is a non-2xx RFC 9457 body. no_data_reason is the 200 no_data status's `reason`. unsupported_code is the 200 unsupported status's `code`. diagnostics_warning is an entry in the response envelope's diagnostics.warnings. done_event_warning is an entry in the SSE stream's terminal done event, for trailing work that finishes after the response event has shipped. manifest_warning is an entry in a Datasets manifest's warnings array or a Datasets options envelope's warnings array. flagged_match_gate is the failed quality-gate code in a 200 no_data response's `flagged_match.gate`, naming why the nearest real match was flagged in place of a confident answer. skills_warning is an entry in a Skills API list body's or skill body's warnings array. observations_omission_reason is a returned Spec's `observations.omitted.reason`, the 200 response's own explanation for why that Spec's observations preview carries no series. The Datasets observations body carries the same `omitted` object. mcp_refusal is the refusal envelope in an MCP tool result's structured content, a `code` with a `retry_mode`, a `message` and a `fix`, plus `retry_after_s`, `upstream_code` and `capability_version` where a server sends them, which has no HTTP status.", "enum": [ "problem_details", "no_data_reason", @@ -156,7 +159,8 @@ "manifest_warning", "flagged_match_gate", "skills_warning", - "observations_omission_reason" + "observations_omission_reason", + "mcp_refusal" ] }, "http_status": { @@ -191,6 +195,44 @@ } } }, + { + "if": { + "properties": { + "endpoint": { + "const": "mcp.tools" + } + }, + "required": [ + "endpoint" + ] + }, + "then": { + "properties": { + "channel": { + "const": "mcp_refusal" + } + } + } + }, + { + "if": { + "properties": { + "channel": { + "const": "mcp_refusal" + } + }, + "required": [ + "channel" + ] + }, + "then": { + "properties": { + "endpoint": { + "const": "mcp.tools" + } + } + } + }, { "if": { "properties": { @@ -261,6 +303,8 @@ "datasets.refresh", "datasets.operations", "datasets.analysis", + "datasets.exports", + "datasets.export_link", "graph.node", "graph.nodes", "graph.members", @@ -272,7 +316,8 @@ "platform.codes", "skills.list", "skills.fetch", - "skills.resource" + "skills.resource", + "mcp.tools" ] } }, diff --git a/tools/tests/test_validate_client_contract.py b/tools/tests/test_validate_client_contract.py index 700a78e..f17cd0e 100644 --- a/tools/tests/test_validate_client_contract.py +++ b/tools/tests/test_validate_client_contract.py @@ -1424,6 +1424,18 @@ def change(doc: Any) -> None: _add_action({"code": "spec_not_found", "status": 404, "condition": "x"}), "'spec_not_found' is also a code in registries/codes.json", ), + ( + "proxy-codes.json", + _add_action({"code": "binding_mismatch", "status": 404, "condition": "x"}), + "'binding_mismatch' is also a code in registries/codes.json", + ), + ( + "proxy-codes.json", + _add_action( + {"code": "alternates_unevaluated", "status": 404, "condition": "x"} + ), + "'alternates_unevaluated' is also a code in registries/codes.json", + ), ( "warning-codes.json", _drop_action("ref_digest_changed"), @@ -2206,7 +2218,9 @@ def change(doc: Any) -> None: "proxy-problem-without-retryable", "proxy-problem-in-plain-json", "accepted-operation-reads-done", - "proxy-code-is-a-registry-code", + "proxy-code-is-a-problem-registry-code", + "proxy-code-is-a-no-data-registry-code", + "proxy-code-is-a-warning-registry-code", "warning-code-ref-digest-changed-missing", "warning-code-missing", "warning-code-series-left-out-missing", diff --git a/tools/tests/test_validate_registry.py b/tools/tests/test_validate_registry.py index 2255e44..6328f57 100644 --- a/tools/tests/test_validate_registry.py +++ b/tools/tests/test_validate_registry.py @@ -14,6 +14,9 @@ Also tests check_entries' precondition for that matching: an entry's `mappings` may not repeat an `(endpoint, channel)` pair. +Also tests the schema's coupling of `mappings`: doors to endpoints, and +`mcp.tools` to `mcp_refusal` in both directions. + Run with: uv run --with jsonschema --with pyarrow --with pytest pytest tools/tests -q (importing tools/validate_registry.py imports jsonschema at module scope, so a plain interpreter needs it on the path; `--with` provisions it for the @@ -148,15 +151,24 @@ def test_new_mapping_with_channel_outside_classification_fails(check: CheckFn) - @pytest.mark.parametrize( - ("classification", "expected"), - [("warning", []), ("refusal", ["manifest_warning"])], + ("endpoint", "channel", "classification", "expected"), + [ + ("datasets.mint", "manifest_warning", "warning", []), + ("datasets.mint", "manifest_warning", "refusal", ["manifest_warning"]), + ("mcp.tools", "mcp_refusal", "refusal", []), + ("mcp.tools", "mcp_refusal", "warning", ["mcp_refusal"]), + ], ) -def test_manifest_warning_mapping_requires_warning_entry( - check: CheckFn, classification: str, expected: list[str] +def test_new_mapping_channel_suits_classification( + check: CheckFn, + endpoint: str, + channel: str, + classification: str, + expected: list[str], ) -> None: new_mappings = [ *_OLD_MAPPINGS, - {"endpoint": "datasets.mint", "channel": "manifest_warning"}, + {"endpoint": endpoint, "channel": channel}, ] problems = check( _entry(new_mappings, classification=classification), @@ -360,3 +372,25 @@ def test_schema_rejects_invalid_search_doors( "http_status": 400, } assert list(schema_validator.iter_errors(mapping)) + + +@pytest.mark.parametrize( + ("mapping", "valid"), + [ + ({"endpoint": "mcp.tools", "channel": "mcp_refusal"}, True), + ( + { + "endpoint": "mcp.tools", + "channel": "problem_details", + "http_status": 400, + }, + False, + ), + ({"endpoint": "mcp.tools", "channel": "no_data_reason"}, False), + ({"endpoint": "datasets.mint", "channel": "mcp_refusal"}, False), + ], +) +def test_schema_couples_mcp_tools_and_mcp_refusal( + schema_validator: Draft202012Validator, mapping: dict[str, Any], valid: bool +) -> None: + assert schema_validator.is_valid(mapping) is valid diff --git a/tools/validate_client_contract.py b/tools/validate_client_contract.py index 9f1502f..2f03997 100644 --- a/tools/validate_client_contract.py +++ b/tools/validate_client_contract.py @@ -34,8 +34,10 @@ retryable flag `by_status`. Each reason the key-lookup rule raises is a row of class `AuthenticationError`. - `proxy-codes.json` and `warning-codes.json` list codes in order, each once, - none of them a code another table or `registries/codes.json` holds, and the - warning codes are those the client rules issue: `ref_digest_changed`, + none of them a code another table or `registries/codes.json` holds, except + that a proxy code may share a registry code whose every mapping is + `mcp_refusal`. The warning codes are those the client rules issue: + `ref_digest_changed`, `route_deprecated`, `server_warning`, one `key_` per key-format reason, and each warning the series-left-out rule's vectors name. - Each scenario's file name matches its `name`, names are unique, `expect` @@ -498,8 +500,8 @@ def _sorted_once(label: str, codes: Sequence[str]) -> list[str]: def _check_other_codes( label: str, codes: Sequence[str], taken: Mapping[str, set[str]] ) -> list[str]: - """A code table's codes are sorted, listed once, and none is a code - another table or the registry holds.""" + """A code table's codes are sorted, listed once, and none is a code in + `taken`, the code sets the table may not share.""" problems = _sorted_once(label, codes) for source, others in taken.items(): problems.extend( @@ -4252,13 +4254,25 @@ def validate() -> list[str]: "client-codes.json": {row["code"] for row in client_rows}, } proxy_codes = _load(PACKAGE_DIR / "proxy-codes.json") + # A client never meets a code carried only in MCP tool results, so a proxy code + # may share its spelling. + mcp_only = { + entry["code"] + for entry in registry["entries"] + if all(mapping["channel"] == "mcp_refusal" for mapping in entry["mappings"]) + } errors = _schema_problems( "proxy-codes.json", validators["proxy-codes.schema.json"], proxy_codes ) problems.extend( errors or _check_other_codes( - "proxy-codes.json", [row["code"] for row in proxy_codes["codes"]], taken + "proxy-codes.json", + [row["code"] for row in proxy_codes["codes"]], + { + **taken, + "registries/codes.json": taken["registries/codes.json"] - mcp_only, + }, ) ) proxy_rows = [] if errors else proxy_codes["codes"] diff --git a/tools/validate_registry.py b/tools/validate_registry.py index ca42030..041aea8 100644 --- a/tools/validate_registry.py +++ b/tools/validate_registry.py @@ -50,6 +50,8 @@ # so this channel legally carries either classification depending on the # entry. "observations_omission_reason", + # The refusal envelope of an MCP tool result, which has no HTTP status. + "mcp_refusal", } WARNING_CHANNELS = { "diagnostics_warning",